CoreAdvanced Text Animationsno dependencies

GlyphCascade

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 props
32
2
6

Installation

Copies the component source and its CSS Module into your project.

npx motionfoundry add glyph-cascade

Writes 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

PropTypeDefault
text

Target text string to decode.

string'DETERMINISTIC GLYPH...'
direction

Order in which characters lock into their final glyph.

'ltr' | 'rtl' | 'center''ltr'
scrambleSpeed

Milliseconds per scramble tick.

number32
cascadeStep

Number of scramble iterations before each character locks.

number2
blurAmount

Blur applied to unresolved characters in pixels.

number6
charset

Pool of temporary cipher glyphs.

string'01/#%&*+<>ΔΛΞ'

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}>&nbsp;</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}