Text Animations

Scramble Text

Decryption-style text animation — characters churn through random glyphs and lock in from left to right, triggered by viewport or hover.

Bring this into your project

Run this command in your project folder. The CLI adds the source and dependencies; then follow the usage example below.

npx webberui@latest add scramble-text.en

New project or installation trouble? Start here →

Playground

Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.

0.05
Active edited usageDefault props
<ScrambleText/>

The complete example includes the preview layout and data. Install the component first, then paste this file into your project.

Installation

npx webberui@latest add https://webberui.com/r/scramble-text.en.json

Or, once registries are configured in components.json, install it as @webberui/scramble-text.

Usage

import { ScrambleText } from "@/components/ui/scramble-text";

<ScrambleText
  text="WEBBERUI DECRYPTED"
  className="font-mono text-4xl font-bold"
/>

Hover-to-replay mode, and mixed Chinese/English is no problem:

<ScrambleText text="Hover me: decryption animation" trigger="hover" duration={0.04} />

A custom scramble character set (for example a hacker-style 0 and 1):

<ScrambleText text="BINARY MODE" charset="01" className="font-mono" />

Scrambled from the very first paint (SSR-safe), with screen readers announcing the real text when it changes:

<ScrambleText text={status} initialScrambled live="polite" />

Props

PropTypeDefaultDescription
textstring—The target text; Chinese and emoji are both fine
durationnumber0.05Interval at which each character locks in (seconds); smaller decrypts faster
charsetstringalphanumerics + symbolsThe character set used for the scramble churn
trigger"view" | "hover""view"Play automatically on entering the viewport, or replay on mouse hover
oncebooleantrueWith trigger="view", plays only the first time the element enters the viewport
initialScrambledbooleanfalseShow deterministic-hash glyphs instead of the answer on the first render (including SSR); server and browser output match
live"off" | "polite" | "assertive""off"aria-live level of the screen-reader text; off adds no aria-live

How it works

  • At its core is a single requestAnimationFrame loop: it computes how many characters have locked in from the elapsed time (one more character locks from the left every duration seconds), and every 2 frames the unlocked positions draw a new random glyph from charset, so the per-frame flicker is not too harsh. The animation writes straight into the DOM text node and does not trigger a React re-render; the previous rAF is cancelled on unmount and on replay
  • Grapheme splitting uses Intl.Segmenter, so Chinese characters, emoji, and combining characters each count as one unit. Locking in restores the original character directly, so Chinese target text works exactly the same, while the scramble churn always uses charset
  • Whitespace does not scramble, keeping line width and line breaking stable throughout the animation
  • Character width jitter: scrambled glyphs have different widths, so the layout jumps sideways. Pairing with font-mono (a monospaced font) is recommended, or at least tabular-nums in mostly numeric scenarios. When the target text contains Chinese, the width difference between Chinese and Latin makes the jitter more obvious, and a monospaced font works best
  • Accessibility: the complete text lives in an sr-only element and the animation layer is marked aria-hidden, so the screen reader always reads a complete sentence instead of scrambled glyphs; when the user has "reduce motion" enabled at the system level, the final text is shown directly with no animation
  • initialScrambled: by default the first render outputs the answer itself (hidden via opacity until it enters the viewport when trigger="view"). When enabled, the first render outputs scrambled glyphs instead; each glyph comes from an FNV-1a hash of "grapheme content + position" mapped into charset rather than Math.random, so server and browser produce the same string and there is no hydration mismatch. Since the first paint is already scrambled and does not leak the answer, view mode no longer hides it first. With trigger="hover" the text stays scrambled until the first mouse enter
  • live: the real text lives in the sr-only layer; live="polite" or "assertive" adds aria-live to that layer, so when text changes the screen reader announces the complete new text once, while the scrambled layer stays aria-hidden and is never read out
  • With once={false} and trigger="view", leaving and re-entering the viewport replays the animation; with trigger="hover" the text is shown statically and decrypts again on every mouse enter