Character-by-character cryptographic cascade reveal with temporary glyph scrambling, progressive blur reduction, selectable text, and directional sweep control.
Preview
DETERMINISTIC GLYPH CASCADE // v2.0
Customize
5 props32
2
6
Installation
Copies the component source and its CSS Module into your project.
npx motionfoundry add glyph-cascadeWrites components/ui/glyph-cascade.tsx and components/styles/glyph-cascade.module.css.
Usage
Updates live as you change the controls — only props that differ from the defaults are included.
example.tsx
1import { GlyphCascade } from "@motionfoundry/ui";2 3export function Example() {4 return (5 <GlyphCascade />6 );7}Props
Accessibility
Renders an accessible sr-only text node alongside aria-hidden visual glyph cells while preserving full user text selection once resolved. Skips scrambling under prefers-reduced-motion.
Toggle prefers-reduced-motion in your OS or browser devtools (Rendering → Emulate CSS media feature) to verify the reduced-motion state.
Source
components/ui/glyph-cascade.tsx
1"use client";2 3import * as React from "react";4import styles from "../styles/glyph-cascade.module.css";5 6export interface GlyphCascadeProps {7 /** Target string to reveal */8 text: string;9 /** Direction of character resolution */10 direction?: "ltr" | "rtl" | "center";11 /** Interval in ms between scramble frames */12 scrambleSpeed?: number;13 /** Scramble iterations per character */14 cascadeStep?: number;15 /** Blur radius in px for unresolved glyphs */16 blurAmount?: number;17 /** Cipher character pool */18 charset?: string;19 /** Optional className */20 className?: string;21}22 23export function GlyphCascade({24 text,25 direction = "ltr",26 scrambleSpeed = 32,27 cascadeStep = 2,28 blurAmount = 6,29 charset = "01/#%&*+<>ΔΛΞ",30 className = "",31}: GlyphCascadeProps) {32 const [step, setStep] = React.useState(0);33 const [reducedMotion, setReducedMotion] = React.useState(false);34 35 React.useEffect(() => {36 if (typeof window === "undefined" || !window.matchMedia) return;37 const mq = window.matchMedia("(prefers-reduced-motion: reduce)");38 setReducedMotion(mq.matches);39 const handler = (e: MediaQueryListEvent) => setReducedMotion(e.matches);40 mq.addEventListener("change", handler);41 return () => mq.removeEventListener("change", handler);42 }, []);43 44 const chars = React.useMemo(() => Array.from(text), [text]);45 const maxSteps = chars.length * Math.max(1, cascadeStep);46 47 React.useEffect(() => {48 if (reducedMotion) {49 setStep(maxSteps);50 return;51 }52 setStep(0);53 const timer = window.setInterval(() => {54 setStep((prev) => {55 if (prev >= maxSteps) {56 window.clearInterval(timer);57 return maxSteps;58 }59 return prev + 1;60 });61 }, scrambleSpeed);62 return () => window.clearInterval(timer);63 }, [text, direction, scrambleSpeed, maxSteps, reducedMotion]);64 65 const isCharResolved = (index: number) => {66 if (reducedMotion || step >= maxSteps) return true;67 const total = chars.length;68 let orderIndex = index;69 if (direction === "rtl") {70 orderIndex = total - 1 - index;71 } else if (direction === "center") {72 const mid = (total - 1) / 2;73 orderIndex = Math.abs(index - mid) * 1.8;74 }75 return step >= orderIndex * cascadeStep;76 };77 78 return (79 <span className={`${styles.root} ${className}`.trim()}>80 <span className={styles.srOnly}>{text}</span>81 <span aria-hidden="true" className={styles.glyphTrack}>82 {chars.map((ch, idx) => {83 if (ch === " ") {84 return <span key={idx} className={styles.space}> </span>;85 }86 const resolved = isCharResolved(idx);87 const pool = charset.length > 0 ? charset : "01X#";88 const cipherChar = pool[(idx * 7 + step) % pool.length];89 return (90 <span91 key={idx}92 className={`${styles.glyph} ${resolved ? styles.resolved : styles.scrambling}`}93 style={94 resolved95 ? undefined96 : ({97 filter: `blur(${ Math.max(0, blurAmount * 0.5) }px)`,98 } as React.CSSProperties)99 }100 >101 {resolved ? ch : cipherChar}102 </span>103 );104 })}105 </span>106 </span>107 );108}