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
<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
Pairs with
Conflicts with
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.