Issue №131 Summer 2026A journal of dev tools, libraries & small ideasUpdated weekly
frontend · 4 min read
frontendJune 25, 2026 · 4 min

glow-card-react

glow-card-react is a tiny React library that adds a cursor-tracking glow to any card — a soft radial spotlight that follows the pointer and fades out when it leaves. It ships a drop-in `<GlowCard>` component and a headless `useGlowCard` hook, works with both React 18 and 19, and has zero runtime dep

glow-card-react — live demo screenshot
01
frontend

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-react

There'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

About the author
Er An Khoo