Text

Animated Text

Character-level text animation — typewriter, scramble and flowing gradient as one component's variants.

Preview in your theme

Loading preview…

"use client"

import * as React from "react"
import { cn } from "@/lib/utils"

const KEYFRAMES = `@keyframes at-gradient{to{background-position:200% center}}`

const SCRAMBLE_CHARS = "!<>-_\\/[]{}=+*^?#"

// render has to stay pure (react-hooks/purity): a deterministic hash of (step, i) instead of
// Math.random — step changes every tick, so it still looks scrambled
function scrambleChar(step: number, i: number) {
  return SCRAMBLE_CHARS[(Math.imul(step * 31 + i * 17 + 7, 2654435761) >>> 0) % SCRAMBLE_CHARS.length]
}

Installation

npx shadcn@latest add https://ui.zyeon.ai/r/animated-text.json

Prompt

The prompt behind this component — paste it into your AI assistant to recreate or adapt it.

Build a React + TypeScript + Tailwind "AnimatedText" component.

Contract
- Export a forwardRef span extending React.HTMLAttributes<HTMLSpanElement>.
- Props: text (string), variant = "typewriter" | "scramble" | "gradient"
  (default "typewriter"), speed (ms per character step, default 45),
  delay (ms before starting, default 0).

Behavior
- Keep ONE piece of state: progress (how many characters are settled). A
  setTimeout(delay) arms a setInterval(speed) that increments progress and
  self-clears at text.length; both timers are cleaned up on unmount.
- The displayed string is DERIVED at render, never stored:
  - typewriter: text.slice(0, progress) plus a blinking caret span.
  - scramble: settled prefix + the remaining characters replaced by glyphs
    from a symbol set. Use a deterministic hash of (progress, index) instead
    of Math.random so render stays pure (React Compiler purity rule) — the
    tail still re-scrambles every tick because progress changes.
  - gradient: no timers at all — a CSS keyframe slides a 200%-wide
    background gradient across the glyphs via background-clip: text.
- Reset progress when text/variant/speed/delay change using the
  adjust-state-during-render pattern (compare a prev key, no effects).
- Read prefers-reduced-motion via useSyncExternalStore on matchMedia
  (server snapshot false); when reduced, render the full text immediately
  and stop the gradient loop with motion-reduce:[animation:none].

Rendering & styling
- Semantic tokens only. The gradient runs from-foreground via-primary/50
  to-foreground — the 50% alpha on the middle stop guarantees a visible
  sweep even on monochrome themes where primary equals foreground
  (e.g. shadcn's default zinc); caret is bg-current.
- Accessibility: the animated glyphs are noise for screen readers — put the
  real text in aria-label on the wrapper and aria-hidden on the animated
  span. The gradient variant keeps real DOM text.
- Ship the gradient @keyframes via a React 19 hoisted
  <style href precedence> tag — no Tailwind config edits, dedupes by href.

Customization levers
- Pace: speed 30–80ms reads well for headlines; longer text wants faster.
- Scramble glyph set: swap the symbol string for a domain flavor
  (binary "01", katakana, hex) without touching the engine.
- Gradient hue: change via-primary/50 to another token (via-chart-2/60
  etc.) — keep some alpha so monochrome themes still show the sweep; tune
  the 3s duration for a calmer or flashier pace.
- Caret: restyle the w-px bg-current span (block cursor: w-[0.5em]), or
  remove it for a subtitle feel.
- Looping: wrap with a key that increments on an interval to replay, or
  cycle through an array of texts at the call site.

Concepts

  • Derived display, single state — only the settled-character count lives in state; the visible string is computed at render, so the three variants share one tiny engine instead of three text buffers.
  • Deterministic scramble — a pure hash of (progress, index) picks each unsettled glyph; render stays pure for the React Compiler while the tail still re-rolls every tick.
  • Screen-reader honesty — assistive tech gets the final text at once via aria-label; the animated intermediate frames are aria-hidden noise it never hears.
  • CSS-only gradient — the gradient variant is zero-JS after mount: background-clip: text plus a background-position keyframe, stopped under reduced motion.
  • Adjust-state-during-render — prop changes reset the animation by comparing a previous key during render (the React-documented pattern), avoiding effect-driven state cascades.

On This Page