---
name: text-scramble-effect
description: Use when you want "Techy, cyber, dramatic" - Randomizes characters before resolving into readable text.
---

# Text Scramble Effect

> **Category:** Motion / Interaction  -  **Personality:** Techy, cyber, dramatic
>
> **Best use:** AI, cybersecurity, futuristic brands
>
> **Ratings:** Professional 3/5 - Casual 3/5 - Exotic 5/5 - Visual impact 4/5 - Difficulty 3/5 - Performance cost 2/5

## What it is

A "decode" effect: a piece of text first appears as a flicker of random glyphs, then settles character-by-character into the real words. Drive it on a loop to cycle a hero through several phrases, or fire it once on hover so a label "re-decodes" itself. It reads as machine intelligence resolving signal out of noise, which is why AI products, cybersecurity dashboards and futuristic brands reach for it. Pure vanilla JS — a small queue of per-character timings stepped by `requestAnimationFrame`.

## Dependencies / CDN

None - vanilla JS. The demo only pulls two web fonts (optional); a **monospace** face is strongly recommended for the scrambling text so character swaps don't shift the layout width:

```html
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Sora:wght@400;500;600;700&family=Space+Mono:wght@400;700&display=swap" rel="stylesheet">
```

## HTML

A target element for the looping headline, plus any number of `data-text` elements that re-scramble on hover. Seed the real text in the markup so it's readable with JS off / reduced-motion:

```html
<h2 class="headline"><span class="text">DECRYPTING THE SIGNAL</span><span class="caret" aria-hidden="true"></span></h2>

<nav>
  <button class="scrambit" data-text="Platform">Platform</button>
  <button class="scrambit" data-text="Threat Intel">Threat Intel</button>
  <button class="scrambit" data-text="Docs">Docs</button>
</nav>
```

## CSS

The effect itself is a single rule — colour the transient glyphs so they read as "noise". Everything else is theming. Use a monospace font on scrambling text:

```css
.headline, .scrambit { font-family: 'Space Mono', monospace; }   /* fixed width = no jitter */

.dud {                              /* the random characters mid-decode */
  color: #4af2c8;
  opacity: .92;
  text-shadow: 0 0 12px rgba(74,242,200,.5);   /* subtle glow sells the "signal" */
}

/* blinking terminal caret (optional flourish) */
.caret { display:inline-block; width:.58ch; height:.96em; margin-left:.1em;
  background:#4af2c8; animation:caret 1.05s steps(1) infinite }
@keyframes caret { 50% { opacity:0 } }

@media (prefers-reduced-motion: reduce){
  .caret { animation:none }
  .dud   { text-shadow:none }
}
```

## JavaScript

The whole engine is one small class. `set(text)` builds a queue giving each character a random `start`/`end` frame, then `update()` walks the queue every frame: before `start` show the old char, between `start` and `end` show a random glyph (wrapped in `.dud`), after `end` lock in the target char. It resolves a Promise when every character has landed.

```js
var GLYPHS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789#%&*<>[]{}/\\|=+?!:;';
function esc(c){ return c==='&'?'&amp;':c==='<'?'&lt;':c==='>'?'&gt;':c; } // duds may be < > &

function Scramble(el, chars){
  this.el=el; this.chars=chars||GLYPHS; this.queue=[]; this.frame=0; this.req=0;
  this.update=this.update.bind(this);
}
Scramble.prototype.rand=function(){ return this.chars.charAt(Math.random()*this.chars.length|0); };
Scramble.prototype.set=function(txt){
  var old=this.el.textContent||'', len=Math.max(old.length,txt.length), self=this;
  this.queue=[];
  for(var i=0;i<len;i++){
    var start=Math.random()*36|0, end=start+12+(Math.random()*36|0);
    this.queue.push({from:old.charAt(i),to:txt.charAt(i),start:start,end:end,ch:''});
  }
  if(this.req) cancelAnimationFrame(this.req);
  this.frame=0;
  return new Promise(function(res){ self.done=res; self.update(); });
};
Scramble.prototype.update=function(){
  var out='', complete=0, q;
  for(var i=0;i<this.queue.length;i++){
    q=this.queue[i];
    if(this.frame>=q.end){ complete++; out+=esc(q.to); }
    else if(this.frame>=q.start){
      if(!q.ch||Math.random()<0.3){ q.ch=this.rand(); }
      out+='<span class="dud">'+esc(q.ch)+'</span>';
    } else { out+=esc(q.from); }
  }
  this.el.innerHTML=out;
  if(complete===this.queue.length){ this.req=0; if(this.done) this.done(); }
  else { this.frame++; this.req=requestAnimationFrame(this.update); }
};

var REDUCED = matchMedia('(prefers-reduced-motion: reduce)').matches;

// Loop a hero headline through phrases (skipped under reduced-motion → static text)
var phrases=['DECRYPTING THE SIGNAL','HUNTING THE ANOMALY','NEUTRALIZING THREATS','INTELLIGENCE, DECODED'];
var head=document.querySelector('.text');
if(head && !REDUCED){
  var fx=new Scramble(head), n=0;
  (function next(){ fx.set(phrases[n++ % phrases.length]).then(function(){ setTimeout(next,2200); }); })();
}

// Re-scramble any [data-text] element back to its label on hover
[].forEach.call(document.querySelectorAll('.scrambit'), function(el){
  var f=new Scramble(el), label=el.getAttribute('data-text')||el.textContent;
  el.addEventListener('mouseenter', function(){ if(!REDUCED) f.set(label); });
});
```

## How it works

- **Per-character timeline.** Each slot gets a random `start` and `end` frame, so letters resolve at staggered moments — that ragged settle (not a uniform wipe) is what makes it feel like decoding.
- **Three states per character:** `from` (old char) before `start`, a random glyph between `start` and `end`, `to` (final char) after `end`. `complete === queue.length` ends the loop and resolves the Promise.
- **Promise-chained loop.** Because `set()` resolves when the word lands, you chain `.then(next)` to cycle phrases without timers fighting each other.
- **Old text matters.** `set()` reads the *current* `textContent`, so animating A → B morphs from A's letters; hovering an element with its own label re-scrambles it in place.
- **`requestAnimationFrame`** ties the rate to the display refresh and auto-pauses on hidden tabs; `cancelAnimationFrame` on re-entry stops overlapping runs.

## Customization

- **Charset / mood:** swap `GLYPHS`. Katakana (`アイウエオカキ…`) gives a Matrix look; `0123456789` gives a counter (used for the live metric in the demo); braille or box-drawing glyphs read as data.
- **Speed & chaos:** lower the `36` ranges for a snappier decode; raise the `0.3` flicker probability for busier noise; widen the `end - start` gap for a slower, more dramatic settle.
- **Colour:** the entire visual identity is the `.dud` rule — tint + `text-shadow` glow. Resolved text can stay white while glyphs glow an accent.
- **Triggers:** loop (hero), `mouseenter`/`focus` (links), `IntersectionObserver` (decode once on scroll-in), or a button click for an on-demand "re-run".

## Accessibility & performance

- **Honour `prefers-reduced-motion`.** Seed the real words in the HTML and *don't start* the loop or hover handlers when the query matches — the user simply sees final text. (The CSS `@media` block is not enough; the rAF loop must be gated in JS.)
- **Cheap:** it only rewrites the `innerHTML` of small strings each frame — negligible cost. Keep simultaneously-decoding elements modest and let `document.hidden` / rAF idle hidden tabs.
- **Screen readers:** the mid-decode markup is gibberish, so keep these elements short and decorative, and store the real string in `data-text` / a stable label. Avoid `aria-live` on the scrambling node or it will announce noise.

## Gotchas

- **Escape the glyphs.** The classic charset contains `<`, `>` and `&`; injected raw via `innerHTML` they corrupt the markup (a stray `<` swallows following text). Run every emitted character through a tiny `esc()` — see the code.
- **Use a monospace font for scrambling text.** Proportional fonts make the line width jump on every glyph swap, so the layout twitches; monospace keeps it rock-steady.
- **Cancel before restarting.** Re-trigger an element while it's still animating and you get two rAF loops on one node — always `cancelAnimationFrame(this.req)` inside `set()`.
- **Reserve height.** When a looping headline cycles phrases of different lengths, give its container a `min-height` so the page doesn't jump as wrapping changes.
- **Don't read `textContent` mid-flight** to persist state — during animation it's the scrambled value. Keep the canonical string in `data-text` or a variable.
