Scramble / Decode Text

A mono headline and five nav-style rows decode from random glyphs (A–Z, 0–9, #@%&!) into real words on load and re-scramble on hover, while a status line decodes a new word every three seconds.

Loading

AI prompt

Recreate this effect with any AI tool

Paste into Claude, Cursor, v0, Lovable or Bolt. 1254 words of exact values — rewritten live as you tune the Playground.

prompt.md · Scramble / Decode Text
### EFFECT: Scramble / Decode Text
**What it does (visual):** A dark, monospace page. Top-left a small eyebrow: a glowing `#d4ff3a` dot, a status word ("LISTENING") and "— ch. 04". Below it a large, light monospace headline, then a list of five nav rows separated by hairlines — each row has an index ("01"), a big label ("Index", "Studio", "Archive", "Journal", "Contact") and a muted mono meta ("142 entries"). On load the headline and the five labels are *not* readable: they are strings of random glyphs from the `alphanumeric` set (alphanumeric = `ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789#@%&!`, binary = `01`, blocks = `▮▯░▒▓`, lowercase = `a–z`) of exactly the final length. 0.2s after load the headline starts "decoding": the glyphs keep churning for 0.3s, then real letters lock in from left to right (from the right when rightToLeft = false) until the full phrase "Signal from noise" is resolved 1.2s later. The five labels decode the same way, starting at 0.5s and each 0.1s after the previous, while their rows fade in. A footnote fades in at 1.6s. Afterwards: hovering (or keyboard-focusing) a row re-encodes just its label for 0.8s and draws a `#d4ff3a` hairline under the row; and every 3 seconds the status word decodes into the next one in the cycle LISTENING → DECODING → TRANSMITTING → RENDERING. The dot pulses slowly the whole time.

**Tech stack:**
- GSAP 3.13+ with ScrambleTextPlugin (free since 3.13). No ScrollTrigger needed.
- CDN: `<script src="https://cdn.jsdelivr.net/npm/gsap@3/dist/gsap.min.js"></script>` + `<script src="https://cdn.jsdelivr.net/npm/gsap@3/dist/ScrambleTextPlugin.min.js"></script>`
- npm: `import gsap from "gsap"; import { ScrambleTextPlugin } from "gsap/ScrambleTextPlugin"; gsap.registerPlugin(ScrambleTextPlugin);`
- Font: a monospace with a light weight (demo: JetBrains Mono 300) — essential so scrambled glyphs don't change the line width.

**HTML structure:**
```html
<section class="scr">
  <div class="scr__inner">
    <p class="scr__eyebrow"><span class="scr__dot" aria-hidden="true"></span><span class="scr__status" data-words="LISTENING|DECODING|TRANSMITTING|RENDERING">LISTENING</span><span class="scr__channel">— ch. 04</span></p>
    <h1 class="scr__title">Signal from noise</h1>
    <nav class="scr__nav" aria-label="Sections">
      <a class="scr__item" href="#index"><span class="scr__index">01</span><span class="scr__label">Index</span><span class="scr__meta">142 entries</span></a>
      <a class="scr__item" href="#studio"><span class="scr__index">02</span><span class="scr__label">Studio</span><span class="scr__meta">Oslo · Lisbon</span></a>
      <a class="scr__item" href="#archive"><span class="scr__index">03</span><span class="scr__label">Archive</span><span class="scr__meta">2014 — 2026</span></a>
      <a class="scr__item" href="#journal"><span class="scr__index">04</span><span class="scr__label">Journal</span><span class="scr__meta">Weekly</span></a>
      <a class="scr__item" href="#contact"><span class="scr__index">05</span><span class="scr__label">Contact</span><span class="scr__meta">Open for Q1</span></a>
    </nav>
    <p class="scr__footnote">Hover an item to re-encode it.</p>
  </div>
</section>
```

**CSS requirements:**
- `.scr`: `min-height: 100vh; display: grid; place-items: center; padding: 8vw 6vw; background: var(--bg, #0a0a0a); color: var(--text, #ededed); font-family: var(--font-mono);`
- `.scr__inner`: `width: 100%; max-width: 1100px;`
- `.scr__eyebrow`: `display: flex; align-items: center; gap: .9em; font-size: 11px; text-transform: uppercase; letter-spacing: .14em; color: var(--muted);` `.scr__status { color: var(--text); min-width: 13ch; }` (reserves the width of the longest word so the line never shifts).
- `.scr__dot`: `width: 7px; height: 7px; border-radius: 50%; background: var(--accent, #d4ff3a); box-shadow: 0 0 12px var(--accent);`
- `.scr__title`: `font-weight: 300; font-size: clamp(2.4rem, 7vw, 6.5rem); line-height: 1; letter-spacing: -0.04em; margin: .6em 0 .7em;`
- `.scr__nav`: `display: grid; border-top: 1px solid var(--line);`
- `.scr__item`: `position: relative; display: grid; grid-template-columns: 3.5ch 1fr auto; align-items: baseline; gap: clamp(1rem, 3vw, 3rem); padding: clamp(.9rem, 1.8vw, 1.4rem) 0; border-bottom: 1px solid var(--line); text-decoration: none; color: inherit;`
- `.scr__item::after`: `content: ''; position: absolute; left: 0; bottom: -1px; height: 1px; width: 100%; background: var(--accent); transform: scaleX(0); transform-origin: left; transition: transform .5s cubic-bezier(.22, 1, .36, 1);` → `scaleX(1)` on `:hover` and `:focus-visible`.
- `.scr__index`, `.scr__meta`, `.scr__footnote`: 11px uppercase `letter-spacing: .12em; color: var(--muted);`
- `.scr__label`: `font-weight: 300; font-size: clamp(1.3rem, 3.2vw, 2.6rem); letter-spacing: -0.03em; line-height: 1;`
- Under 768px: `.scr__item { grid-template-columns: 3ch 1fr; } .scr__meta { display: none; }`

**Animation spec (exact values):**
Constants: `CHARS` = the `alphanumeric` glyph set (see above), `SPEED = 0.6`, `REVEAL_DELAY = 0.3`, `DURATION = 1.2`, `RTL = false`, `CYCLE = 3`.
1. Before anything renders: store each decode target's real copy in `el.dataset.text` and replace its `textContent` with a random string from `CHARS` of the same length (title + the five labels).
2. Load timeline `tl = gsap.timeline({ defaults: { ease: 'none' } })`:
   - `tl.from('.scr__eyebrow', { opacity: 0, duration: 0.6 }, 0)`
   - `tl.to(title, { duration: 1.2, scrambleText: { text: title.dataset.text, chars: CHARS, speed: 0.6, revealDelay: 0.3, tweenLength: false, rightToLeft: false } }, 0.2)` — 0.3s of noise, then letters resolve left → right, finished at 0.2 + 1.2s.
   - `tl.from('.scr__item', { opacity: 0, duration: 0.5, stagger: 0.1 }, 0.5)` — rows fade in.
   - For each label `i` (0–4): `tl.to(label, { duration: 1.2, scrambleText: { text: label.dataset.text, chars: CHARS, speed: 0.6, revealDelay: 0.3, tweenLength: false, rightToLeft: false } }, 0.5 + i * 0.1)`.
   - `tl.from('.scr__footnote', { opacity: 0, duration: 0.6 }, 1.6)`.
3. Status cycle (words from `data-words`, split on `|`):
   ```js
   let w = 0;
   const cycle = () => { w = (w + 1) % words.length;
     gsap.to(status, { duration: 1, ease: 'none', scrambleText: { text: words[w], chars: CHARS, speed: 0.6, revealDelay: 0.2, tweenLength: true, rightToLeft: false },
       onComplete: () => gsap.delayedCall(3, cycle) }); };
   gsap.delayedCall(3, cycle);
   ```
   (`delayedCall` — not `setInterval` — so the cycle pauses with `gsap.globalTimeline`.)
4. Dot: `gsap.to('.scr__dot', { opacity: 0.25, duration: 0.9, yoyo: true, repeat: -1, ease: 'sine.inOut' })`.
5. Hover/focus on a row: `gsap.to(label, { duration: 0.8, ease: 'none', overwrite: 'auto', scrambleText: { text: label.dataset.text, chars: CHARS, speed: 0.6, revealDelay: 0.15, tweenLength: false, rightToLeft: false } })`.

**Interaction:** `pointerenter` and `focus` on `.scr__item` re-scramble that row's label (0.8s, resolves to the same word) and the lime hairline draws under the row over 0.5s (CSS transition). Leaving does nothing extra — the word is already resolved. Rows are real links.

**Responsive behavior:** All sizes are fluid. Under 768px the meta column is hidden and the row grid becomes `3ch 1fr`. Hover re-scramble simply doesn't occur on touch (no `pointerenter` without a hover-capable pointer); tapping a row follows the link. No `matchMedia` branches are needed for timing — the decode reads well at any size.

**Accessibility:** With `prefers-reduced-motion: reduce` do not pre-scramble or animate anything: the real headline, labels and status are shown immediately, the dot does not pulse and the underline transition is disabled. During the decode the DOM text is noise, so keep the decode short (≤ 1.5s) and keep the `<nav aria-label="Sections">` and `<h1>` semantics; links stay focusable with the underline doubling as the focus indicator (`:focus-visible`). The dot is `aria-hidden`.

**Performance rules:** ScrambleText writes `textContent` ~60×/s per active element — only 6 elements decode at once, which is trivial, but avoid scrambling whole paragraphs. All tweens, delayedCalls and the repeat live inside `gsap.context()`; `ctx.revert()` on unmount restores the original text and kills the cycle. Remove the hover/focus listeners in the same cleanup. The host's `globalTimeline` pause stops the status cycle and dot pulse off-screen.

**Customization knobs:**
- `CHARS` (`alphanumeric`) — the glyph alphabet (`binary` = `'01'` for a terminal feel, `blocks` = `'▮▯░▒▓'`, `lowercase` for softer noise).
- `SPEED` (0.6) — churn rate; `REVEAL_DELAY` (0.3) — how long it stays pure noise; `DURATION` (1.2).
- `CYCLE` (3) and `data-words` — the status vocabulary.
- `tweenLength: false` keeps widths stable; `true` lets short → long words grow.
- `rightToLeft` (false) — resolve from the right instead.
- Accent colour for the dot and hover hairline (`#d4ff3a`).

**Acceptance criteria:**
- [ ] Before 0.2s the headline and labels are unreadable noise of the correct length; no real word flashes first.
- [ ] Headline is fully resolved 1.2s after it starts (0.2s); labels resolve in a 0.1s cascade starting at 0.5s; footnote appears at 1.6s.
- [ ] Hover/focus re-scrambles only that label and draws the accent underline; rapid re-hovers don't stack.
- [ ] Status word changes every 3s via a 1s decode and the line width never shifts.
- [ ] Reduced-motion users see the final text immediately with no scrambling or pulsing.
- [ ] No layout shift (monospace, fixed lengths); no console errors.

Source

Full source code

<section class="scr">
  <div class="scr__inner">
    <p class="scr__eyebrow"><span class="scr__dot" aria-hidden="true"></span><span class="scr__status" data-words="LISTENING|DECODING|TRANSMITTING|RENDERING">LISTENING</span><span class="scr__channel">— ch. 04</span></p>
    <h1 class="scr__title">Signal from noise</h1>
    <nav class="scr__nav" aria-label="Sections">
      <a class="scr__item" href="#index"><span class="scr__index">01</span><span class="scr__label">Index</span><span class="scr__meta">142 entries</span></a>
      <a class="scr__item" href="#studio"><span class="scr__index">02</span><span class="scr__label">Studio</span><span class="scr__meta">Oslo · Lisbon</span></a>
      <a class="scr__item" href="#archive"><span class="scr__index">03</span><span class="scr__label">Archive</span><span class="scr__meta">2014 — 2026</span></a>
      <a class="scr__item" href="#journal"><span class="scr__index">04</span><span class="scr__label">Journal</span><span class="scr__meta">Weekly</span></a>
      <a class="scr__item" href="#contact"><span class="scr__index">05</span><span class="scr__label">Contact</span><span class="scr__meta">Open for Q1</span></a>
    </nav>
    <p class="scr__footnote">Hover an item to re-encode it.</p>
  </div>
</section>