Anatomy of Encrypted Card
How a radial mask and a re-scrambling glyph stream turn a static card into a live ciphertext reveal that follows the pointer.
↓Scroll to step through it
Stream under, content over
EncryptedCard keeps a full glyph field painted underneath the content and
reveals it through a soft radial mask centered on the pointer. The ciphertext
never moves across the surface. The effect is two layers, one CSS variable
pair, and an interval that runs only while you hover.
The root is a group relative overflow-hidden card. An absolute, aria-hidden
glyph layer fills the inset with STREAM_LEN (1500) characters drawn from
DEFAULT_CHARS. Children sit in a sibling at z-raised, so real content
never fights the stream for stacking order.
The stream is filled client-side on mount so SSR markup stays deterministic:
randomString never runs during the first server render.
The reveal is a mask, not a clip of content
Pointer position never moves the glyphs. onPointerMove writes --x and
--y as pixel offsets from the card's bounding rect; the stream's
mask-image is a radial gradient whose center reads those variables (and
--reveal for radius). Defaults are 50% / 50%, so the first paint doesn't
flash from a corner.
Opacity is separate from the mask: the stream starts at opacity-0 and fades
to --stream-opacity on group-hover over 300ms. The mask only decides
where the faded-in stream is visible.
Scramble only while hovered
While hovered is true and reduced motion is off, a setInterval re-rolls
the entire 1500-character string every speed ms (default 55). Leaving the
card clears the interval; entering starts it again. The ciphertext keeps
changing under the spotlight, and no timer runs while the card is idle.
The result
Hover the card and move across it. The spotlight and the scramble are the two mechanisms described above.
One glyph field, one radial mask on --x/--y, and an interval that only
lives for the length of the hover.
- Glyph stream
- absolute inset-0 · STREAM_LEN 1500 · aria-hidden
- Content
- relative z-raised, so children stay on top
<div className="group relative overflow-hidden …"><div aria-hidden className={STREAM_BASE} style={{ color: streamColor }}> {stream}</div><div className="relative z-raised">{children}</div></div>Anatomy of Encrypted Card
How a radial mask and a re-scrambling glyph stream turn a static card into a live ciphertext reveal that follows the pointer.
- Glyph stream
- absolute inset-0 · STREAM_LEN 1500 · aria-hidden
- Content
- relative z-raised, so children stay on top
<div className="group relative overflow-hidden …"><div aria-hidden className={STREAM_BASE} style={{ color: streamColor }}> {stream}</div><div className="relative z-raised">{children}</div></div>Stream under, content over
EncryptedCard keeps a full glyph field painted underneath the content and
reveals it through a soft radial mask centered on the pointer. The ciphertext
never moves across the surface. The effect is two layers, one CSS variable
pair, and an interval that runs only while you hover.
The root is a group relative overflow-hidden card. An absolute, aria-hidden
glyph layer fills the inset with STREAM_LEN (1500) characters drawn from
DEFAULT_CHARS. Children sit in a sibling at z-raised, so real content
never fights the stream for stacking order.
The stream is filled client-side on mount so SSR markup stays deterministic:
randomString never runs during the first server render.
mask-image: radial-gradient(var(--reveal) circle at var(--x) var(--y), #000, transparent)
- Stream (full)
- always painted · opacity gated by group-hover
- Radial mask
- #000 inside → transparent outside · reveals, doesn't hide
el.style.setProperty("--x", `${e.clientX - rect.left}px`);el.style.setProperty("--y", `${e.clientY - rect.top}px`);// STREAM_BASE includes:// [mask-image:radial-gradient(var(--reveal) circle at var(--x,50%) var(--y,50%),#000_0%,transparent_100%)]The reveal is a mask, not a clip of content
Pointer position never moves the glyphs. onPointerMove writes --x and
--y as pixel offsets from the card's bounding rect; the stream's
mask-image is a radial gradient whose center reads those variables (and
--reveal for radius). Defaults are 50% / 50%, so the first paint doesn't
flash from a corner.
Opacity is separate from the mask: the stream starts at opacity-0 and fades
to --stream-opacity on group-hover over 300ms. The mask only decides
where the faded-in stream is visible.
setInterval(() => setStream(randomString(1500, chars)), speed)
- Opacity in
- 300ms ease · group-hover:[opacity:var(--stream-opacity)]
- Re-randomize
- setInterval(speed) · default 55ms · glyphs change, opacity steady
React.useEffect(() => {setStream(randomString(STREAM_LEN, characters));if (reduced || !hovered) return;const id = window.setInterval(() => { setStream(randomString(STREAM_LEN, characters));}, speed);return () => window.clearInterval(id);}, [characters, speed, hovered, reduced]);Scramble only while hovered
While hovered is true and reduced motion is off, a setInterval re-rolls
the entire 1500-character string every speed ms (default 55). Leaving the
card clears the interval; entering starts it again. The ciphertext keeps
changing under the spotlight, and no timer runs while the card is idle.
Production API key
rotated 3 days ago
sk_live_51Nc••••••••4Rf2The result
Hover the card and move across it. The spotlight and the scramble are the two mechanisms described above.
One glyph field, one radial mask on --x/--y, and an interval that only
lives for the length of the hover.
Reduced motion
Under prefers-reduced-motion, the stream still paints and the mask still
tracks, so the ciphertext stays visible. The interval never starts, and
motion-reduce:[transition:none] removes the 300ms opacity fade.
const reduced = usePrefersReducedMotion();
// …interval skipped when reduced || !hovered
// STREAM_BASE: motion-reduce:[transition:none]Motion Score
opacityFade / cross-fade