A cursor-following glow card in two lines of React
There's a particular hover effect that shows up on half the landing pages I admire: you move your mouse over a card, and a soft glow lights up exactly where the cursor is, trailing it as you move and fading out when you leave. It's subtle, it's tactile, and it makes a flat grid of boxes feel alive.
I've hand-rolled it at least four times. Every time it's the same dance — a mousemove listener, some getBoundingClientRect math, a radial gradient, a couple of CSS variables. Every time I get the stacking context slightly wrong and the glow paints over the text instead of behind it. So I finally packaged it: glow-card-react, a tiny, dependency-free library that does exactly this and nothing else.
What it does
Two ways to use it. The drop-in component:
import { GlowCard } from 'glow-card-react';
<GlowCard color="#7c5cff" className="card">
<h3>Pro</h3>
<p>A glow that follows your cursor.</p>
</GlowCard>;<GlowCard> is just a <div> with an aria-hidden glow layer behind your content. You bring the padding, background, and border-radius; the glow adapts to whatever box you give it.
Or the headless hook, if you'd rather own the markup:
const { containerProps, glowProps } = useGlowCard({ color: '#2dd4bf' });
<div {...containerProps} className="tile">
<span {...glowProps} />
<div style={{ position: 'relative', zIndex: 1 }}>Hover me</div>
</div>;A handful of props — color, size, intensity, fadeDuration — and that's the whole surface area for the common case.
How I built it
Two decisions are worth pulling out, because both are easy to get wrong.
Don't re-render on every mouse move. The naive version stores the cursor position in React state and updates it in onPointerMove. That fires a render on every pixel of movement — dozens per second, per card. On a grid of pricing tiles that's a stutter you can feel. So the position never touches React state. The move handler writes straight to the DOM node:
const handlePointerMove = (event) => {
const el = event.currentTarget;
const rect = el.getBoundingClientRect();
el.style.setProperty('--glow-x', `${event.clientX - rect.left}px`);
el.style.setProperty('--glow-y', `${event.clientY - rect.top}px`);
};The glow layer is a radial gradient anchored at var(--glow-x) var(--glow-y). The browser repaints; React does nothing. The only thing that does go through state is the hover-enter/leave toggle, which flips the opacity — and that happens twice per interaction, not sixty times a second.
The React 19 StrictMode trap. This one bit me on a previous project, so I designed around it from the start. If you need a browser API with setup and teardown — an IntersectionObserver, a matchMedia listener — the tempting pattern is to set it up in a ref callback and tear it down in a useEffect. Under React 19's StrictMode, that quietly breaks: the dev-mode remount simulation runs your effect cleanup but does not re-fire the ref callback, so your listener is gone for good. Tests in jsdom won't catch it, because jsdom doesn't simulate the remount dance.
I sidestepped it two ways. The pointer handlers are plain React synthetic events (onPointerMove, onPointerEnter, onPointerLeave) — React owns their attach/detach, so there's nothing to leak. And the one thing that genuinely needs a subscription — watching prefers-reduced-motion — lives entirely inside a single useEffect, with both the addEventListener and the legacy Safari addListener fallbacks cleaned up in the same place. No ref-callback setup anywhere.
Reduced motion gets a real answer, not a kill switch. When a visitor asks their OS to reduce motion, the glow stops chasing the cursor — but it doesn't vanish. It settles into a calm, static, centered highlight with no transition. The card still feels designed; it just stops moving. That's on by default; you opt out with respectReducedMotion={false}.
One small detail I enjoyed: the glow layer uses border-radius: inherit, and a CSS background is always clipped to the border box. So the glow picks up your card's rounded corners automatically, with no overflow: hidden and no configuration.
Try it
pnpm add glow-card-reactThere's a live demo where you can move your cursor across a wall of cards in different colors, radii, and intensities — it's the fastest way to "get it." The whole thing is MIT licensed, ships ESM + CJS + types, and is tested to 100% coverage across statements, branches, functions, and lines.
What's next
A few ideas on the list: an optional border-glow variant (lighting the edge nearest the cursor rather than the surface), a group mode so a shared glow can move across a grid of cards as one, and a touch-friendly fallback. But the core is deliberately small, and I'd like it to stay that way. If you build something with it, I'd love to see it.
End of essay



