← Back to gallery
CSS

Text Scramble Reveal

Real text stays as live DOM underneath; an aria-hidden cipher overlay ticks per-character then clears via clip-path. Three variants: a binary terminal decipher, a mixed-symbol search settle, and a slower hex editorial cipher.

text-scrambledecipherclip-pathsteps-timingaria-hiddenper-char-staggerprefers-reduced-motion

Decipher reveal · steps cadence

Text Scramble Reveal

A scramble overlay ticks through symbol-like glyphs while the real destination phrase remains present, selectable, and screen-reader-readable underneath. Three variants demonstrate a terminal-style binary decipher, a mixed-symbol search result settle, and a hex editorial cipher.

Mono · binary cipher

Terminal Decipher

A monospace terminal-style decipher pass. The noise overlay cycles through binary glyphs (0/1) before clearing to reveal the destination phrase. Steps timing on the overlay; the underlying phrase is real DOM text the whole time.

  • terminal
  • binary
  • steps timing

Mixed glyphs · quick settle

Search Result

A shorter mixed-symbol scramble for search results — makes the result feel processed without delaying comprehension. The destination phrase is visible enough underneath that the effect does not block reading.

  • quick settle
  • mixed glyphs
  • result label

Hex · slow editorial reveal

Editorial Cipher

A slower editorial cipher using hex glyphs (0-9 / A-F). Lets the cipher layer feel intentional without becoming unreadable; the phrase stays moderate length so scramble noise doesn't become visual clutter.

  • headline
  • hex
  • editorial

Scramble inspector

Terminal Decipher

  • terminal
  • binary
  • steps timing

A monospace terminal-style decipher pass. The noise overlay cycles through binary glyphs (0/1) before clearing to reveal the destination phrase. Steps timing on the overlay; the underlying phrase is real DOM text the whole time.

Helped you ship something? 🐟 Send my cat a churu

.scramble-word {
  position: relative;
  display: inline-block;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}

.scramble-word__real {
  /* Real DOM text — stays selectable / readable / accessible. */
  position: relative;
  z-index: 1;
  color: #dcfce7;
}

.scramble-word__noise {
  /* Decorative overlay layered above the real text and cleared
     left-to-right with a clip-path inset. */
  position: absolute;
  inset: 0;
  z-index: 2;
  display: inline-flex;
  justify-content: center;
  color: #34d399;
  pointer-events: none;
  animation: scramble-clear 2.40s steps(12, end) infinite;
}

.scramble-word__noise span {
  /* Each cell ticks on its own short cycle, staggered by index so
     the overlay reads as a flickering cipher row. */
  display: inline-block;
  animation: scramble-tick 0.42s steps(2, end) infinite;
  animation-delay: calc(var(--scramble-index, 0) * -48ms);
}

@keyframes scramble-clear {
  0%, 52%   { opacity: 0.88; clip-path: inset(0 0 0 0); }
  100%      { opacity: 0;    clip-path: inset(0 0 0 100%); }
}

@keyframes scramble-tick {
  50% { transform: translateY(-0.05em); }
}

@media (prefers-reduced-motion: reduce) {
  .scramble-word__noise,
  .scramble-word__noise span {
    animation: none;
    opacity: 0;
  }
}

How to make this

A CSS text scramble reveal keeps the final phrase as real text, overlays aria-hidden cipher cells, then clears that overlay with a stepped clip-path mask.

html
<p class="scramble-word">2  <span class="scramble-word__real">ACCESS GRANTED</span>3  <span class="scramble-word__noise" aria-hidden="true">    <span style="--i:0">A</span><span style="--i:1">C</span>    <span style="--i:2">C</span><span style="--i:3">E</span>    <span style="--i:4">S</span><span style="--i:5">S</span>    <span style="--i:6">&nbsp;</span><span style="--i:7">G</span>    <span style="--i:8">R</span><span style="--i:9">A</span>    <span style="--i:10">N</span><span style="--i:11">T</span>    <span style="--i:12">E</span><span style="--i:13">D</span>  </span></p> <style>.scramble-word {  position: relative;  display: inline-block;  margin: 0;  color: #dcfce7;  font: 800 1.8rem/1 ui-monospace, SFMono-Regular, Menlo, monospace;  letter-spacing: .05em;}.scramble-word__real {  position: relative;  z-index: 1;}.scramble-word__noise {  position: absolute;29  inset: 0;  z-index: 2;  display: inline-flex;  color: #34d399;  text-shadow: 0 0 14px rgba(52, 211, 153, .45);  pointer-events: none;  clip-path: inset(0 0 0 0);  animation: text-scramble-recipe-clear 2.4s steps(14, end) infinite;37}.scramble-word__noise span {  display: inline-block;  animation: text-scramble-recipe-tick .42s steps(2, end) infinite;41  animation-delay: calc(var(--i) * -48ms);}@keyframes text-scramble-recipe-clear {  0%, 52% { opacity: .88; clip-path: inset(0 0 0 0); }  100% { opacity: 0; clip-path: inset(0 0 0 100%); }}@keyframes text-scramble-recipe-tick {  50% { transform: translateY(-.05em); }}50@media (prefers-reduced-motion: reduce) {  .scramble-word__noise,  .scramble-word__noise span {    animation: none;    opacity: 0;  }}</style>

Annotated snippet

  1. Line 2The final phrase is the real text. This preserves selection, copy behavior, translation, and screen reader output while the visual cipher layer animates above it.
    PitfallHow do I make text scramble accessible?

    Render the destination phrase as normal DOM text and mark the animated noise layer aria-hidden. Do not replace the text content itself every frame unless you also manage screen reader announcements carefully.

  2. Line 3The noise layer is aria-hidden because it duplicates the same characters for decoration. It should never become the accessible name of the phrase.
    PitfallHow do I make text scramble accessible?

    Render the destination phrase as normal DOM text and mark the animated noise layer aria-hidden. Do not replace the text content itself every frame unless you also manage screen reader announcements carefully.

  3. Line 29Absolute positioning overlays the cipher cells exactly over the real phrase. pointer-events stays off so interaction and selection target the real text underneath.
  4. Line 37The reveal uses clip-path with steps() timing. A smooth fade looks like a dissolve; stepped clearing reads as a decipher or terminal settle.

    Both reveals run the same clip-path inset(0 0 0 0) → inset(0 0 0 100%) keyframe over 1.8s, so the right-edge cut moves across the same distance. ease-in-out timing slides the cut smoothly — the result reads as a dissolve. steps(6, end) snaps the cut between six discrete positions so the phrase clears in chunks, like a terminal settling on the decoded text.

    PitfallWhy use steps() for a scramble reveal?

    Scramble effects read as discrete decipher frames. steps() creates hard frame changes for the mask and per-cell ticks, while smooth easing reads like a normal fade or slide.

  5. Line 41Each cell has a short two-step tick with a negative index delay. The stagger makes the row feel active without changing the underlying phrase.

    Same six cells, same per-cell tick animation. Without per-cell animation-delay the whole row jumps in unison every cycle — it reads as one blinking word. With staggered negative delays (--i * -50ms) each cell hits its peak at a different time, so the row ripples and looks like cells settling independently — what makes the scramble feel "alive".

    PitfallWhy use steps() for a scramble reveal?

    Scramble effects read as discrete decipher frames. steps() creates hard frame changes for the mask and per-cell ticks, while smooth easing reads like a normal fade or slide.

  6. Line 50Reduced motion removes the cipher layer entirely. The phrase remains visible because the real text was never hidden.
    PitfallWhat should prefers-reduced-motion do for scramble text?

    Hide or freeze the cipher overlay and show the completed phrase. Scramble motion is high-frequency visual noise, so reduced-motion users should not have to wait through the reveal to read the text.

Other pitfalls

Can CSS generate random scramble characters?
Not reliably. CSS can mask, tick, and stagger existing cells, but true random character replacement requires JavaScript. Keep the final phrase as real text either way.
Is clip-path scramble animation supported in browsers?
Inset clip-path animations are supported in current major browsers. Keep the real text visible underneath so older or constrained environments still show the final phrase if the decorative layer fails.
Advanced

Scroll each cipher cell through a letter pool, not just a wiggle

Same 1.8s cycle, same per-cell stagger (var(--i) negative delay), same green decoder palette. BEFORE wiggles a fixed letter in each cell — the content never changes so the cipher reads as "two letters bouncing". AFTER stacks 4 glyphs vertically inside each cell's 1em viewport; an explicit keyframe holds each letter for ~400ms, then snaps the column up by 1em (54ms transition) to expose the next letter — three visible roll-snaps until the final glyph lands. Clean single-letter visibility at rest, blink-fast snap between. Both cycles end at the same moment.

View explanation and full code69 lines

The base recipe wiggles each cell vertically — visually busy but the cell content never changes, so the cipher does not actually look like decoding. Production "decoder" effects scroll each cell through a vertical strip of glyphs, snapping to a new letter on every step until the final character lands. The overlay clip-path still clears the whole cipher at the end of the cycle. Same overlay choreography, same steps()-driven cadence, same per-cell stagger via --i — what changes is that each cell now exposes a different glyph on every tick instead of jiggling the same one.

Paste this as a complete alternative to the base recipe — each cipher cell is now a vertical letter column (5 glyphs) scrubbed by translateY steps(5, end), so the content visibly cycles before the overlay clip-path clears.

html
<p class="decode-word" aria-label="ACCESS GRANTED">  <span class="decode-word__real">ACCESS GRANTED</span>  <span class="decode-word__cipher" aria-hidden="true">    <span class="decode-word__cell" style="--i:0"><i>X</i><i>Q</i><i>7</i><i>%</i><i>A</i></span>    <span class="decode-word__cell" style="--i:1"><i>P</i><i>3</i><i>K</i><i>R</i><i>C</i></span>    <span class="decode-word__cell" style="--i:2"><i>9</i><i>F</i><i>L</i><i>W</i><i>C</i></span>    <span class="decode-word__cell" style="--i:3"><i>$</i><i>D</i><i>B</i><i>N</i><i>E</i></span>    <span class="decode-word__cell" style="--i:4"><i>4</i><i>M</i><i>V</i><i>T</i><i>S</i></span>    <span class="decode-word__cell" style="--i:5"><i>Z</i><i>8</i><i>Y</i><i>J</i><i>S</i></span>    <span class="decode-word__cell" style="--i:6"><i>&nbsp;</i><i>&nbsp;</i><i>&nbsp;</i><i>&nbsp;</i><i>&nbsp;</i></span>    <span class="decode-word__cell" style="--i:7"><i>U</i><i>6</i><i>H</i><i>!</i><i>G</i></span>    <span class="decode-word__cell" style="--i:8"><i>K</i><i>2</i><i>O</i><i>+</i><i>R</i></span>    <span class="decode-word__cell" style="--i:9"><i>5</i><i>W</i><i>X</i><i>?</i><i>A</i></span>    <span class="decode-word__cell" style="--i:10"><i>B</i><i>1</i><i>I</i><i>=</i><i>N</i></span>    <span class="decode-word__cell" style="--i:11"><i>F</i><i>0</i><i>P</i><i>*</i><i>T</i></span>    <span class="decode-word__cell" style="--i:12"><i>L</i><i>Y</i><i>Q</i><i>@</i><i>E</i></span>    <span class="decode-word__cell" style="--i:13"><i>V</i><i>S</i><i>U</i><i>#</i><i>D</i></span>  </span></p> <style>.decode-word {  position: relative;  display: inline-block;  margin: 0;  color: #dcfce7;  font: 800 1.8rem/1 ui-monospace, SFMono-Regular, Menlo, monospace;  letter-spacing: .05em;}.decode-word__real { position: relative; z-index: 1; }.decode-word__cipher {  position: absolute;  inset: 0;  z-index: 2;  display: inline-flex;  color: #34d399;  text-shadow: 0 0 14px rgba(52, 211, 153, .45);  pointer-events: none;  clip-path: inset(0 0 0 0);  animation: decode-clear 2.4s steps(14, end) infinite;}.decode-word__cell {  display: inline-flex;  flex-direction: column;  height: 1em;  overflow: hidden;  font-weight: 800;  animation: decode-scroll .42s steps(5, end) infinite;  animation-delay: calc(var(--i) * -48ms);}.decode-word__cell i {  display: block;  height: 1em;  line-height: 1;  font-style: normal;}@keyframes decode-scroll {  from { transform: translateY(0); }  to   { transform: translateY(-4em); }}@keyframes decode-clear {  0%, 52%   { opacity: .88; clip-path: inset(0 0 0 0); }  100%      { opacity: 0;   clip-path: inset(0 0 0 100%); }}@media (prefers-reduced-motion: reduce) {  .decode-word__cipher,  .decode-word__cell { animation: none; opacity: 0; }}</style>

Notes

Overview

Decipher-style text reveal where the destination phrase stays as live DOM text and a decorative aria-hidden cipher overlay ticks through symbol-like glyphs then clears via clip-path. The base text is selectable + copyable + readable throughout; only the overlay animates.

When to use it

Reach for scramble reveals on terminal-style product surfaces, decoding metaphors (search results, AI inference), and hero string entrances. Skip it for utility chrome — the cipher pattern is loud enough to feel marketing-y. Skip it for content over 30 characters; the per-cell tick budget becomes too long.

How it works

Two stacked DOM layers: a real text node holding the destination string, and an aria-hidden overlay positioned absolutely on top. The overlay swaps characters every ~30–60ms from a glyph pool (Katakana, custom symbols) via requestAnimationFrame or a timed JS loop. The overlay clears left-to-right via clip-path: inset(0 100% 0 0) animating to inset(0 0 0 0) over the reveal duration. Once the clip clears, the overlay text matches the underlying text and you can remove the overlay (or just leave it — the user sees real text only).

Production gotchas

The clip-path direction must travel from right to left from the user’s perspective so the cipher “decodes” toward final state — reversing the inset direction breaks the metaphor. The cipher glyph pool affects the “feel”: pure ASCII reads flat, while mixing Katakana + symbols reads Matrix-coded. Avoid Emoji in the pool — they have variable widths and the cipher overlay jitters. Set font-variant-numeric: tabular-nums on the overlay if the destination contains digits, or the swap animation reflows.

Accessibility

The decorative overlay must be aria-hidden="true" so screen readers ignore the cipher glyphs. The underlying real text is announced normally on page load. Under prefers-reduced-motion: reduce skip the cipher swap and clip-path animation entirely — the real text is already there; no transformation needed. Verify focus order if any controls live inside the scrambled region.

References

Implementation depth

Scramble effects should separate readable content from decorative decoys. Keep the final string in the DOM, use aria-hidden for transient character noise, and end quickly enough that the user is not waiting for text to become useful.

The risk is localization. Grapheme clusters, accents, and non-Latin scripts break naive per-character loops. Use a segmentation strategy or limit scrambling to decorative labels that have a stable accessible name.