# Scramble text

Text that resolves character by character through dithered blocks and glyphs, with no layout shift. Plays on mount, in view, on hover or when the text changes.

> For the complete documentation index, see [llms.txt](/llms.txt). Markdown variants are available by appending `.md` to any URL or sending an `Accept: text/markdown` header. The dithercn agent skill, for building any dither on the engine, is at [/.well-known/agent-skills/dithercn/SKILL.md](/.well-known/agent-skills/dithercn/SKILL.md).

> **This is an example, not the only option.** It is one component built on the dithercn engine. Install it when it is what you were asked for. For any other look, don't stretch its props to fit: write your own field on `DitherShader` ([Write your own dither](/docs/engine/write-your-own.md)), and read this example's source for technique. Its full source is in [https://dithercn.dev/r/scramble-text.json](https://dithercn.dev/r/scramble-text.json), under `files[].content`.

## Installation [#installation]

```bash
npx shadcn@latest add https://dithercn.dev/r/scramble-text.json
```

## Usage [#usage]

```tsx
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 [#examples]

### Hero reveal [#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.

```tsx title="examples/scramble-text-hero.tsx"
import { ScrambleText } from "@/components/dithercn/scramble-text";

export default function ScrambleTextHero() {
  return (
    <section className="flex w-full max-w-2xl flex-col items-center gap-4 px-6 text-center">
      <h2 className="text-3xl leading-[1.08] font-semibold tracking-[-0.035em] text-balance sm:text-5xl">
        <ScrambleText mode="blocks" color="green" duration={700} stagger={26}>
          Every frame accounted for.
        </ScrambleText>
      </h2>
      <p className="text-muted-foreground max-w-md text-sm text-pretty sm:text-base">
        <ScrambleText
          mode="blocks"
          color="grey"
          delay={500}
          duration={380}
          stagger={6}
        >
          Render logs stream in as they land, so you can see what shipped and
          when.
        </ScrambleText>
      </p>
    </section>
  );
}
```

### Nav links on hover [#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.

```tsx title="examples/scramble-text-hover.tsx"
import {
  ScrambleText,
  SCRAMBLE_MONO,
} from "@/components/dithercn/scramble-text";

const LINKS = ["Docs", "Components", "Changelog", "Pricing"];

export default function ScrambleTextHover() {
  return (
    <nav
      aria-label="Example"
      className="bg-background flex flex-wrap items-center justify-center gap-1 rounded-full p-1.5"
    >
      {LINKS.map((label) => (
        <a
          key={label}
          href="#scramble-hover"
          className="text-muted-foreground hover:text-foreground hover:bg-muted focus-visible:ring-ring/50 rounded-full px-3.5 py-2 font-mono text-xs tracking-[0.12em] uppercase transition-colors duration-150 outline-none focus-visible:ring-[3px]"
        >
          <ScrambleText
            trigger="hover"
            mode="glyphs"
            characters={SCRAMBLE_MONO}
            duration={260}
            stagger={22}
          >
            {label}
          </ScrambleText>
        </a>
      ))}
    </nav>
  );
}
```

### Status that changes [#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.

```tsx title="examples/scramble-text-status.tsx"
"use client";

import { useEffect, useState } from "react";

import {
  ScrambleText,
  SCRAMBLE_HEX,
} from "@/components/dithercn/scramble-text";
import { cssColor } from "@/lib/dither";

const STATES = [
  { color: "orange", detail: "build 7f3a2c1 · eu-west", label: "Deploying" },
  { color: "blue", detail: "checks 42 of 42 passed", label: "Verifying" },
  { color: "green", detail: "rev 7f3a2c1 · 38 ms p50", label: "Live" },
] as const;

export default function ScrambleTextStatus() {
  const [step, setStep] = useState(0);

  useEffect(() => {
    const timer = setInterval(
      () => setStep((value) => (value + 1) % STATES.length),
      2800
    );
    return () => clearInterval(timer);
  }, []);

  const state = STATES[step] ?? STATES[0];

  return (
    <div className="bg-background flex w-full max-w-xs flex-col gap-3 rounded-2xl p-5">
      <div className="flex items-center justify-between">
        <span className="text-muted-foreground text-xs">Production</span>
        <span className="text-muted-foreground font-mono text-[11px]">
          main
        </span>
      </div>
      <span className="flex items-center gap-2.5">
        <span
          aria-hidden="true"
          className="size-2 rounded-[1px] transition-colors duration-500"
          style={{ backgroundColor: cssColor(state.color) }}
        />
        <ScrambleText
          trigger="change"
          color={state.color}
          caret="running"
          className="text-xl font-medium tracking-tight"
          aria-live="polite"
        >
          {state.label}
        </ScrambleText>
      </span>
      <ScrambleText
        trigger="change"
        mode="glyphs"
        characters={SCRAMBLE_HEX}
        color="grey"
        delay={120}
        stagger={14}
        className="text-muted-foreground font-mono text-xs"
      >
        {state.detail}
      </ScrambleText>
    </div>
  );
}
```

## Props [#props]

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `children` (required) | `string` |  | The final text. Change it to scramble from the old text to the new one. |
| `as` | `"h1" \| "h2" \| "h3" \| "h4" \| "p" \| "span" \| "div"` | `"span"` | The element to render. |
| `trigger` | `"mount" \| "in-view" \| "hover" \| "change"` | `"in-view"` | When to play. Hover also listens to the closest link or button, including keyboard focus. Change plays only when the text changes. |
| `mode` | `"dither" \| "blocks" \| "glyphs"` | `"dither"` | How unsettled characters look: blocks then glyphs, only blocks, or only glyphs. |
| `characters` | `string` | `the text's own letters` | Glyphs to cycle through. SCRAMBLE_MONO and SCRAMBLE_HEX are exported. |
| `color` | `DitherColor` | `"ink"` | Colour of blocks, glyphs and the caret: a palette name or any CSS colour. Settled letters fade from it to the text colour. |
| `duration` | `number` | `520` | Milliseconds one character takes to resolve. |
| `stagger` | `number` | `34` | Milliseconds between one character starting and the next. |
| `delay` | `number` | `0` | Milliseconds before the first character starts. |
| `speed` | `number` | `1` | Multiplies every timing. 2 plays twice as fast. |
| `rate` | `number` | `20` | Glyph changes per second while a character is unsettled. |
| `caret` | `"none" \| "running" \| "always"` | `"none"` | A caret at the leading edge while the text resolves. Always keeps it blinking after the last character. |
| `cell` | `number` | `font size / 22` | Dither cell size in CSS pixels, at least 1. |
| `onComplete` | `() => void` |  | Called when every character has settled. |

## Notes [#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 `mount` or `in-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 `always` caret is shown), and the frame loop stops as soon as every character settles.