Rejouice — loading0

Text scramble

A label resolves from random characters into its real text on hover or on entering the viewport.

Grade: ARuntimes: cssStatus: code-only

Documented, no live demo yet — the code below is complete.

Intent

A cheap, legible way to give a small piece of type a mechanical, transmitting quality. It suits eyebrow labels and nav items where the word is short enough that the resolution is instantaneous rather than a puzzle.

Implementation

The real word stays in a visually hidden span; only the aria-hidden layer is ever mutated.

Dependencies: none

html-css-js
<button class="scramble" data-scramble>
  <span class="sr-only">Contact</span>
  <span class="scramble__layer" aria-hidden="true">Contact</span>
</button>

<style>
.scramble__layer {
  display: inline-block;
  font-variant-numeric: tabular-nums;
  letter-spacing: var(--tracking-caption, 0.08em);
  text-transform: uppercase;
}
</style>

<script type="module">
if (!matchMedia("(prefers-reduced-motion: reduce)").matches) {
  const POOL = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
  const FRAMES = 8;

  for (const root of document.querySelectorAll("[data-scramble]")) {
    const layer = root.querySelector(".scramble__layer");
    const final = layer.textContent;
    let running = false;

    root.addEventListener("pointerenter", () => {
      if (running) return;
      running = true;

      let frame = 0;
      const tick = () => {
        const locked = Math.floor(frame / FRAMES);
        layer.textContent = final
          .split("")
          .map((char, i) =>
            i < locked || char === " " ? char : POOL[Math.floor(Math.random() * POOL.length)],
          )
          .join("");

        frame += 1;
        if (locked < final.length) requestAnimationFrame(tick);
        else {
          layer.textContent = final;
          running = false;
        }
      };

      requestAnimationFrame(tick);
    });
  }
}
</script>

Use it when

  • Short uppercase labels: nav items, eyebrows, buttons of one or two words.

Avoid it when

  • On sentences or anything longer than about twelve characters — it becomes unreadable noise.
  • On the same element as a masked reveal; two competing entrances on one word reads as a glitch.

Accessibility

  • Never mutate the element that carries the accessible name — screen readers will announce the scrambled garbage. Scramble an aria-hidden layer only.
  • Skip entirely under reduced motion.
  • Keep the element's width fixed with a monospace layer or `ch` sizing so the label does not reflow while resolving.

Performance

  • One `rAF` per active element, stopped as soon as the word resolves.
  • Do not start a second run while one is in flight — track it and bail.

Knobs

pool
Characters to scramble through. Default: ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789
frames
Frames before a character locks. Default: 8

Composition

Instruction for Claude Code

Add a text scramble effect for short uppercase labels. Keep the real word in a visually hidden span as the accessible name and scramble only an aria-hidden sibling layer — never mutate the accessible text. On pointerenter, run a rAF loop that locks one character every 8 frames from left to right, filling the rest from an A–Z0–9 pool, preserving spaces, and guard against overlapping runs with a running flag. Use tabular-nums and fixed letter spacing so the label does not reflow. Skip the whole script under prefers-reduced-motion, and do not apply it to anything longer than about twelve characters.