Making Screen Capture Feel Less Boring

Most screen capture tools look the same — dim overlay, crosshair, done. We added custom themes with particles and WebGL shaders to Snapr's selection overlay. Here's why we built it and how it works.

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.