Installation
pnpm dlx shadcn@latest add https://dithercn.dev/r/scramble-text.jsonUsage
import { ScrambleText } from "@/components/dithercn/scramble-text";
export function Title() {
return <ScrambleText as="h2">Every frame accounted for</ScrambleText>;
}Each character resolves on its own, a few milliseconds after the one before it. In the default dither mode a block in the accent colour thickens through the levels of a 4×4 Bayer matrix, a few glyphs flicker, and the letter lands, fading from the accent colour to the text colour. blocks skips the glyphs and glyphs skips the blocks.
The glyphs come from the text itself, so a headline scrambles in its own typeface instead of falling back to a terminal font. Pass characters for a specific set: SCRAMBLE_MONO and SCRAMBLE_HEX are exported.
Change children and only the characters that differ scramble, from the old text to the new one. In the demo above, the last words change every few seconds.
Every placeholder sits in the slot of the character it becomes, and the real text stays underneath, so the width never changes and nothing around it moves.
Examples
Hero reveal
mode="blocks" with a colour: each letter fills in from a sparse dither to solid before it appears. The paragraph uses a short duration and a small stagger so a long line still resolves quickly.
Nav links on hover
With trigger="hover", the text replays whenever the closest link or button is hovered or receives keyboard focus. mode="glyphs" and a short duration keep it quick enough for something people hover often.
Status that changes
trigger="change" never plays on its own; it plays when the text changes. The status label scrambles only the letters that differ and picks up the colour of each state, with a caret while it resolves. The detail line uses hex glyphs.
Props
| Prop | Type | Default |
|---|---|---|
| children* The final text. Change it to scramble from the old text to the new one. | Type: string | Default – |
| as The element to render. | Type: "h1" | "h2" | "h3" | "h4" | "p" | "span" | "div" | Default "span" |
| trigger When to play. Hover also listens to the closest link or button, including keyboard focus. Change plays only when the text changes. | Type: "mount" | "in-view" | "hover" | "change" | Default "in-view" |
| mode How unsettled characters look: blocks then glyphs, only blocks, or only glyphs. | Type: "dither" | "blocks" | "glyphs" | Default "dither" |
| characters Glyphs to cycle through. SCRAMBLE_MONO and SCRAMBLE_HEX are exported. | Type: string | Default the text's own letters |
| color Colour of blocks, glyphs and the caret: a palette name or any CSS colour. Settled letters fade from it to the text colour. | Type: DitherColor | Default "ink" |
| duration Milliseconds one character takes to resolve. | Type: number | Default 520 |
| stagger Milliseconds between one character starting and the next. | Type: number | Default 34 |
| delay Milliseconds before the first character starts. | Type: number | Default 0 |
| speed Multiplies every timing. 2 plays twice as fast. | Type: number | Default 1 |
| rate Glyph changes per second while a character is unsettled. | Type: number | Default 20 |
| caret A caret at the leading edge while the text resolves. Always keeps it blinking after the last character. | Type: "none" | "running" | "always" | Default "none" |
| cell Dither cell size in CSS pixels, at least 1. | Type: number | Default font size / 22 |
| onComplete Called when every character has settled. | Type: () => void | Default – |
Notes
- The final text is always in the page as real text. While the effect plays it turns transparent and an overlay hidden from assistive technology draws the blocks and glyphs on top, so screen readers, search engines and copy and paste only ever see the final text. For a status that changes, add
aria-live="polite"yourself. - With
mountorin-view, the text waits hidden until it plays. Without JavaScript it shows normally. - With reduced motion turned on, the text never scrambles and the caret does not blink.
- The overlay exists only while the effect plays (or while an
alwayscaret is shown), and the frame loop stops as soon as every character settles.