Building the Site's Signal-Field Background Without an Animation Library
The latest commit I pushed to this site added a “signal field” effect — a faint grid that drifts slowly, plus small particles floating in the background, and a glow that follows the cursor position. No canvas, no animation library.
The grid and particles are pure CSS. The grid is built from two linear-gradients (horizontal and vertical lines) tiled into a 64x64px pattern, animated with @keyframes gridDrift to shift position over time. The particles are just empty <span> elements whose position, size, animation duration, and delay are randomized in the Astro component’s frontmatter (Math.random()), then animated to float up and down (floatY) and pulse their opacity (pulseGlow), also with plain CSS.
An interesting detail: positions are randomized at build time, not at runtime. Because Math.random() runs in an Astro component’s frontmatter, it executes once when the site is built into static HTML — not on every page load. The effect: each page (home, a blog post, a portfolio entry) gets its own particle layout, since each is rendered separately at build time, but the same visitor opening the same page repeatedly will always see the exact same particle pattern. That’s a trade-off I’m fine with in exchange for not needing JavaScript to generate positions on the client.
The only JavaScript that runs is for the cursor-following glow — and even that is tightly scoped: it only activates on devices with hover support and a precise pointer (matchMedia('(hover: hover) and (pointer: fine)')), so it never attaches a listener that’s useless on phones or tablets. There’s also a prefers-reduced-motion check before the listener is even attached.
Accessibility wasn’t an afterthought. Under prefers-reduced-motion: reduce, the grid stops moving, the glow transition is disabled, and particles stop animating with a static opacity instead. Under prefers-reduced-transparency: reduce, and when printing the page, the entire effect is hidden outright — there’s no reason to force a decorative effect in those conditions.
The takeaway: a small-scale decorative animation like this doesn’t need a heavy library. CSS @keyframes plus a bit of build-time randomization is enough for something that feels alive, and the accessibility constraints (reduced motion, reduced transparency, print) actually deserved more attention than the animation itself.