Building the Site's Signal-Field Background Without an Animation Library

The latest commit I pushed to this site added a “signal field” effect to the background — a faint grid that drifts slowly, small particles floating around, plus a glow that follows the cursor. No canvas, no animation library. All CSS, plus a tightly scoped bit of JavaScript.
Grid and particles are pure CSS
The grid is built from two linear-gradients — one horizontal, one vertical — tiled into a 64x64px pattern, animated to shift position via @keyframes gridDrift. The particles are just empty <span> elements whose position, size, animation duration, and delay get randomized in the Astro component’s frontmatter using Math.random(), then animated to float up and down (floatY) and pulse their opacity (pulseGlow) — also plain CSS, not a single line of JS moving them.
The detail I find interesting: the randomization runs at build time, not runtime. Since Math.random() executes in Astro’s frontmatter, it only fires once, when the site gets built into static HTML. The effect is a little unexpected — every page (home, each blog post, each portfolio entry) ends up with its own particle layout, since each one renders separately at build time. But the same visitor opening the same page repeatedly will keep seeing the identical particle pattern, because the positions are already “frozen” into the generated HTML. That’s a trade-off I’m fine with, in exchange for not needing JavaScript to generate positions on the client every time a page loads.
The only JavaScript that runs
Is for the cursor-following glow. Even that’s tightly scoped — it only activates on devices with hover support and a precise pointer (matchMedia('(hover: hover) and (pointer: fine)')), so it never wires up an event listener that’s useless on a phone or tablet. There’s also a prefers-reduced-motion check before the listener ever gets attached — if that’s set, the listener never fires at all.
Accessibility wasn’t an afterthought
Under prefers-reduced-motion: reduce, the grid stops moving, the glow transition gets disabled, and particles stop animating with a static opacity — not just slowed down, actually stopped. Under prefers-reduced-transparency: reduce, and when printing the page, the whole effect gets hidden entirely. There’s no reason to force a decorative effect under those conditions, and honestly this part took more of my time than the animation itself — thinking through every combination of preference that could apply, instead of just building one “default” path that looks good in a demo.
The takeaway: a small decorative animation like this doesn’t need a heavy library. CSS @keyframes plus a bit of build-time randomization is enough to make something feel alive. What actually deserves more attention isn’t the animation — it’s the accessibility constraints around it.