---
title: Scramble
description: Text that decodes quietly into place.
group: Text
order: 27
new: true
---

Glyphs flicker, then settle into the real phrase. Trigger it with a button or bump `play` from your own state.

```tsx
import { useState } from "react"
import { Scramble } from "@/components/motion/scramble"
import { Button } from "@/components/ui/button"

export function Example() {
  const [play, setPlay] = useState(0)

  return (
    <>
      <Scramble text="Token ready" play={play} />
      <Button onClick={() => setPlay((n) => n + 1)}>Scramble</Button>
    </>
  )
}
```

## Installation

```bash
npx @arctis-sh/@arctis-sh/ui@latest add scramble
```

## Usage

```tsx
import { useState } from "react"
import { Scramble } from "@/components/motion/scramble"
import { Button } from "@/components/ui/button"
```

```tsx
const [play, setPlay] = useState(0)

<>
  <Scramble text="Token ready" play={play} />
  <Button onClick={() => setPlay((n) => n + 1)}>Scramble</Button>
</>
```

## API Reference

### Scramble

| Prop | Type | Default |
| --- | --- | --- |
| text | string | — |
| play | number | 0 |
| duration | number | 1100 |
| speed | number | 28 |
| characters | string | ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789 |
| className | string | — |

Also accepts the other native `span` attributes. Duration and speed are in milliseconds.

## Source

```tsx
"use client";

import "@/lib/motion-styles";

import {
  useEffect,
  useRef,
  useState,
  type ComponentProps,
} from "react";
import { cn } from "@/lib/utils";

const DEFAULT_CHARS =
  "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";

export type ScrambleProps = Omit<ComponentProps<"span">, "children"> & {
  /** Final text to decode into. */
  text: string;
  /** Bump this to replay the scramble. */
  play?: number;
  /** Full decode length in ms. */
  duration?: number;
  /** Glyph tick rate in ms. */
  speed?: number;
  /** Pool of characters used while decoding. */
  characters?: string;
};

function randomChar(pool: string) {
  if (!pool) return "?";
  return pool[Math.floor(Math.random() * pool.length)] ?? "?";
}

function Scramble({
  className,
  text,
  play = 0,
  duration = 1100,
  speed = 28,
  characters = DEFAULT_CHARS,
  ...props
}: ScrambleProps) {
  const [display, setDisplay] = useState(text);
  const frameRef = useRef(0);
  const timerRef = useRef<number | null>(null);
  const textRef = useRef(text);
  textRef.current = text;

  useEffect(() => {
    const target = textRef.current;
    const reduced =
      typeof window !== "undefined" &&
      window.matchMedia("(prefers-reduced-motion: reduce)").matches;

    if (reduced || target.length === 0) {
      setDisplay(target);
      return;
    }

    if (timerRef.current !== null) {
      window.clearInterval(timerRef.current);
      timerRef.current = null;
    }

    const tickMs = Math.max(speed, 12);
    const totalTicks = Math.max(Math.round(duration / tickMs), 8);
    const pool = characters.length > 0 ? characters : DEFAULT_CHARS;
    frameRef.current = 0;

    setDisplay(
      Array.from(target, (char) => (char === " " ? " " : randomChar(pool))).join(
        "",
      ),
    );

    timerRef.current = window.setInterval(() => {
      frameRef.current += 1;
      const frame = frameRef.current;
      const chars = Array.from(target);
      const next = chars.map((char, i) => {
        if (char === " ") return " ";
        const settleAt = Math.floor((i / Math.max(chars.length, 1)) * totalTicks);
        if (frame >= settleAt + 2) return char;
        return randomChar(pool);
      });
      setDisplay(next.join(""));

      if (frame >= totalTicks + 2) {
        setDisplay(target);
        if (timerRef.current !== null) {
          window.clearInterval(timerRef.current);
          timerRef.current = null;
        }
      }
    }, tickMs);

    return () => {
      if (timerRef.current !== null) {
        window.clearInterval(timerRef.current);
        timerRef.current = null;
      }
    };
  }, [text, play, duration, speed, characters]);

  return (
    <span
      data-slot="scramble"
      className={cn("arctis-scramble inline-block whitespace-pre-wrap", className)}
      aria-label={text}
      {...props}
    >
      {display}
    </span>
  );
}

export { Scramble, DEFAULT_CHARS as SCRAMBLE_DEFAULT_CHARS };
```
