# Scramble text

Text whose characters cycle through katakana, braille, CP437, PETSCII or symbol glyphs and settle left to right, without moving the layout.

> 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 asciicn agent skill, for building any ASCII effect on the engine, is at [/.well-known/agent-skills/asciicn/SKILL.md](/.well-known/agent-skills/asciicn/SKILL.md).

> **This is an example, not the only option.** It is one component built on the asciicn 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 `AsciiShader` ([Write your own](/docs/engine/write-your-own.md)), or draw a CPU grid with `AsciiGrid`, and read this example's source for technique. Its full source is in [https://asciicn.dev/r/scramble-text.json](https://asciicn.dev/r/scramble-text.json), under `files[].content`.

## Installation [#installation]

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

## Usage [#usage]

```tsx
import { ScrambleText } from "@/components/asciicn/scramble-text";

export function Headline() {
  return (
    <ScrambleText as="h2" glyphs="katakana" className="text-4xl font-bold">
      Decrypting the signal
    </ScrambleText>
  );
}
```

Every character starts on a random glyph from the set, changes `rate` times a second, and settles on its letter after `duration` seconds plus `stagger` seconds per character before it, so the text resolves left to right. Spaces stay spaces.

The final text is always laid out and the scrambling glyph is painted over each letter's slot, centred on it. Nothing the animation does can move the layout, even in a proportional font or with glyphs wider than the letters they cover.

## Examples [#examples]

### Hero [#hero]

Two lines that play once on mount: a binary eyebrow and a braille headline.

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

export default function ScrambleTextHero() {
  return (
    <div className="flex max-w-2xl flex-col gap-4">
      <ScrambleText
        as="p"
        glyphs="binary"
        trigger="mount"
        duration={0.3}
        color="green"
        className="font-mono text-xs tracking-widest text-muted-foreground uppercase"
      >
        v2.0 now in beta
      </ScrambleText>
      <ScrambleText
        as="h3"
        glyphs="braille"
        trigger="mount"
        duration={0.7}
        stagger={0.04}
        className="font-display text-4xl leading-tight font-bold tracking-tight text-balance sm:text-6xl"
      >
        Every pixel, a character.
      </ScrambleText>
    </div>
  );
}
```

### Navigation on hover [#navigation-on-hover]

`trigger="hover"` replays a short CP437 scramble each time the pointer enters a link.

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

const LINKS = ["Overview", "Components", "Engine", "Changelog"] as const;

export default function ScrambleTextHover() {
  return (
    <nav aria-label="Product">
      <ul className="flex flex-wrap justify-center gap-x-8 gap-y-3 font-mono text-sm">
        {LINKS.map((label) => (
          <li key={label}>
            <a
              href="#top"
              className="rounded-sm text-muted-foreground outline-offset-4 hover:text-foreground focus-visible:outline-2 focus-visible:outline-ring"
            >
              <ScrambleText
                glyphs="cp437"
                trigger="hover"
                duration={0.2}
                stagger={0.025}
              >
                {label}
              </ScrambleText>
            </a>
          </li>
        ))}
      </ul>
    </nav>
  );
}
```

### Live status [#live-status]

Changing the text plays the scramble again, so a status label can decode into each new state. The live region announces the settled word.

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

import { useEffect, useState } from "react";

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

const STATES = [
  { label: "QUEUED", tone: "grey" },
  { label: "BUILDING", tone: "orange" },
  { label: "DEPLOYING", tone: "blue" },
  { label: "LIVE", tone: "green" },
] as const;

export default function ScrambleTextStatus() {
  const [step, setStep] = useState(0);
  useEffect(() => {
    const timer = window.setInterval(
      () => setStep((value) => (value + 1) % STATES.length),
      2200
    );
    return () => window.clearInterval(timer);
  }, []);
  const state = STATES[step] ?? STATES[0];

  return (
    <p className="flex items-baseline gap-3 font-mono text-sm">
      <span className="text-muted-foreground">deploy/4182</span>
      <ScrambleText
        glyphs="hex"
        trigger="mount"
        duration={0.25}
        color={state.tone}
        aria-live="polite"
        className="inline-block w-[9ch]"
        style={{ color: `var(--ascii-color-${state.tone})` }}
      >
        {state.label}
      </ScrambleText>
    </p>
  );
}
```

## Props [#props]

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `children` | `string` |  | The final text. It is what screen readers, search and copy get. |
| `as` | `"h1" \| "h2" \| "h3" \| "h4" \| "p" \| "span" \| "div"` | `"span"` | The element to render. |
| `glyphs` | `"katakana" \| "braille" \| "cp437" \| "petscii" \| "blocks" \| "symbols" \| "hex" \| "binary"` | `"katakana"` | The glyph set characters cycle through. |
| `characters` | `string` |  | Your own glyphs to cycle through. Wins over glyphs. |
| `trigger` | `"mount" \| "view" \| "hover" \| "loop"` | `"view"` | When it plays. loop plays again after pause seconds. Changing the text always plays it. |
| `duration` | `number` | `0.5` | Seconds every character scrambles before the first one settles. |
| `stagger` | `number` | `0.035` | Seconds between neighbours settling, left to right. |
| `delay` | `number` | `0` | Seconds to wait before starting. |
| `rate` | `number` | `20` | New glyphs per second while scrambling. |
| `pause` | `number` | `2.5` | loop only: seconds to hold the settled text before playing again. |
| `color` | `AsciiColor` | `"ink"` | Colour of the scrambling glyphs. Settled letters keep the text colour. |
| `onComplete` | `() => void` |  | Called each time the text has fully settled. |

Other props pass through to the element.

## Notes [#notes]

* Screen readers get the final text once, from a visually hidden copy. The animated letters are `aria-hidden`. Copying the visible text copies the final string, never the glyphs, because the glyphs are drawn by CSS pseudo-elements.
* With reduced motion, nothing scrambles: the text shows as is.
* `view` plays when the text scrolls into view. Leaving the screen mid-scramble settles every letter at once.
* Katakana and some CP437 glyphs come from a fallback font when the glyph font lacks them. They are centred over each letter, so a wider glyph never pushes its neighbours.
* One `requestAnimationFrame` loop runs while letters are scrambling and stops when they settle. React does not re-render per frame.