Every screen capture tool looks the same when you're selecting a region. Dim overlay. Crosshair cursor. A rectangle with dashed lines. Maybe some coordinates in a tiny label.
It's functional. But it hasn't changed in a long time. If capture is part of your daily workflow, you interact with this UI constantly — and it always feels exactly the same. It's the kind of design that says "this is a utility, not an experience."
I wanted to change that. Not because it needed to be flashy, but because a tool you reach for every day should feel like it's yours.
The idea#
What if the selection overlay — the rectangle you drag across the screen — had a visual personality? Not just a color. An actual animated effect that makes each capture feel a little different.
We called them highlight themes. The concept is simple: when you enter capture mode and drag a selection, the border and the area inside it come alive. Particles drifting through your selection. A hex grid pulsing with light. Snowflakes falling inside the rectangle.
It might sound like a playful little feature. I thought so too, until I started using them daily. There's something unexpectedly satisfying about seeing fireflies drift across your selection while you line up a screenshot. It turns a mechanical action into a small moment.
How it works#
The overlay is a transparent window that spans all displays. When you drag a selection, three layers render on top of each other:
1. Canvas base layer. This handles the dim overlay, the selection rectangle, the inner glow gradient, resize handles, and dimension labels. Standard 2D canvas — nothing fancy here. Every theme uses this layer.
2. Particle effects. Some themes add animated particles inside the selection bounds. We use tsParticles for this — it handles the physics, rendering, and lifecycle of hundreds of particles efficiently. The particles are clipped to the selection area using CSS clip-path, so they only appear inside the rectangle.
Each particle theme ships with a particles.json config that defines the behavior. Here's the Firefly theme — 80 lime-green particles bouncing around with a glow shadow:
{
"particles": {
"number": { "value": 80 },
"color": { "value": "#a3e635" },
"shape": { "type": "circle" },
"opacity": {
"value": { "min": 0.1, "max": 0.7 },
"animation": { "enable": true, "speed": 1.5 }
},
"size": {
"value": { "min": 2, "max": 5 },
"animation": { "enable": true, "speed": 2 }
},
"move": {
"enable": true,
"speed": { "min": 0.3, "max": 1 },
"direction": "none",
"outModes": { "default": "bounce" }
},
"shadow": {
"enable": true,
"color": "#a3e635",
"blur": 10
}
}
}
No code. Just a JSON file describing what the particles look like and how they behave. Swap the color to #b4d7ff, change direction to "bottom", and you've got snowfall.
3. WebGL shaders. For effects that particles can't express — continuous fields, procedural patterns, complex animations — we use WebGL fragment shaders.
Why WebGL specifically, and not just let theme authors write arbitrary JavaScript? Sandboxing. A GLSL shader runs entirely on the GPU within a tightly constrained execution model. It receives a few numeric uniforms (uTime, uResolution, uDpr) and outputs pixel colors. That's it — no arbitrary code execution, no access to app internals. This means we can load and run user-created shaders without auditing every line.
Here's a snippet from the Hex Glow theme — an animated hexagonal grid with pulsing cyan light:
precision mediump float;
uniform vec2 uResolution;
uniform float uTime;
varying vec2 vUv;
vec3 glowColor = vec3(0.0, 0.867, 1.0); // cyan #00ddff
float hexDistance(vec2 p) {
vec2 q = abs(p * vec2(1.732 * 0.5, 0.75));
float d = dot(q, vec2(-0.5, 0.866)) - 1.732 * 0.25;
return (d > 0.0) ? min(d, 1.732 * 0.9 - q.x) : min(-d, q.x);
}
void main() {
vec2 uv = (vUv - 0.5) * vec2(uResolution.x / uResolution.y, 1.0);
vec2 pos = uv - vec2(uTime * 0.03, uTime * 0.02);
float d = hexDistance(fract(pos * 4.0) * 2.0 - 1.0);
float pulse = 0.85 + 0.15 * sin(uTime * 1.5);
float edge = 1.0 - smoothstep(0.0, 0.008, d);
vec3 rgb = glowColor * edge * 1.5 * pulse;
gl_FragColor = vec4(rgb, edge * 0.8);
}
A few dozen lines of math, and you get a living, breathing hex grid. The shader compiles once, runs on the GPU, and costs almost nothing on the CPU side.
Theme as a file format#
Every theme is a folder:
~/.snapr/themes/my-theme/
├── theme.json # Colors, line widths, effect type
├── particles.json # tsParticles config (if particle effect)
└── effect.glsl # Fragment shader (if WebGL effect)
The theme.json manifest defines the visual properties and declares which effect to use. Here's the Firefly theme:
{
"id": "firefly",
"name": "Firefly",
"author": "Snapr",
"stroke": "#a3e635",
"dimBg": "rgba(0,0,0,0.35)",
"fillAlpha": 0.05,
"lineWidth": { "region": 2, "window": 3, "screen": 4 },
"effect": { "type": "tsparticles", "config": "particles.json" }
}
Stroke color, overlay darkness, line widths per selection mode, and an effect pointing to the particle config. For a WebGL theme, you'd swap the effect to { "type": "webgl", "config": "effect.glsl" }.
This means anyone can create a custom theme by writing a JSON file and optionally a GLSL shader or particle config. No code compilation. No plugin API. Just drop a folder into ~/.snapr/themes/ and hit "Rescan" in settings.
What I learned#
People actually care about this. I expected themes to be a novelty that users try once and forget. Instead, theme selection became one of the most-used settings. People have preferences. Some want minimal and clean. Some want particles. A few wrote their own shaders.
Constraints help. The theme system is deliberately limited. You can change colors, line widths, and effects — but you can't change the shape of the selection, the label format, or the handle behavior. This keeps every theme functional. A theme can look wildly different but the interaction always works the same way.
Performance matters more than you'd think. The overlay runs on top of everything, including full-screen apps and games. A shader that drops frames makes the entire capture experience feel broken. We throttle WebGL effects to render every other frame on lower-end hardware, and particle counts are capped in the config to prevent GPU overload.
The skeleton theme was a turning point. My close friend (onejae) wrote a GLSL shader that renders an animated skeleton pulling ropes attached to the selection corners. Over 300 lines of procedural geometry — skull, spine, limbs, joints, all computed in the fragment shader. It's unexpected and delightful. When he sent it over and I saw it running in the overlay, I knew the theme system had legs beyond what I originally imagined.
Why bother#
Screen capture is a utility. Nobody needs animated particles in their selection overlay. But there's a difference between a tool you tolerate and a tool you enjoy. Themes don't make Snapr more productive. They make it more personal. And for something you reach for every day, that matters more than I expected.
Snapr is a screen capture tool for macOS and Windows that ships with 7 built-in highlight themes and full support for custom themes using particles and WebGL shaders. Screenshots, recording, scroll capture, annotation, and editing — all in one app, free with no watermarks or time limits.
Snapr