ScrambledText

Glyph shuffle that resolves into readable copy.

Live demo

01

Each section mounts a different combination of order, pool, and replayOnHover. Auto-start variants run on mount; the hover one waits for your cursor.

Default Β· order="left-to-right"

Letters resolve in reading order, which feels deliberate β€” like a teleprinter or decoding sequence revealing one position at a time.

Random order Β· order="random"

Each character picks its own settle time uniformly across the duration window. The reveal feels organic and Bourne-Identity-ish.

Long copy Β· sentence with random order

Spaces are preserved during scramble so word boundaries form before the letters do. Useful for hero headlines.

Replay on hover Β· order="random" Β· pool="!@#$%01"

Hovers re-trigger the scramble. Custom pool of symbols + digits gives a terminal-grade "access granted" feel.

$ hover to rescramble

Numeric pool Β· order="random" Β· pool="0123456789"

Restricting the pool to digits creates a slot-machine effect β€” perfect for scoreboards, stats, or pricing reveals.

Katakana pool Β· order="random"

Pulling glyphs from a Katakana pool gives the cipher a Matrix-rain texture before the Latin letters land.

Staggered countdown Β· staged delays

Three left-to-right scrambles, each with their own delay. ScrambledText runs once per mount, so the trio fires in sequence and settles.

Badge Β· short live indicator

A pill-shaped badge with a fast scramble and a punctuation pool β€” readable instantly.

Implementation

02
ScrambledText.svelte
<script lang="ts">
  import ScrambledText from '$lib/components/ScrambledText.svelte';
</script>
​
<ScrambledText text="DECODED" duration={1500} order="random" replayOnHover />

ScrambledText computes a per-character settle time once at start, then runs a single requestAnimationFrame loop that swaps in random glyphs from the pool until each character's settle time elapses. When prefers-reduced-motion is on, the rAF loop never spins up β€” the final text renders immediately.

Logic explainer

03

What Does It Do? (Plain English)

ScrambledText displays a string where each character starts as a random glyph from a configurable pool (default A–Z + 0–9) and "decodes" to the final letter at a per-character settle time. The classic Mission Impossible / Matrix / heist-movie terminal reveal β€” every glyph rolls through the pool until the right one drops into place.

Think of it as a row of slot-machine wheels, each spinning at random until its target letter clicks into position.

How It Works (Pseudo-Code)

state:
  display       = ''             // frame-by-frame visible string
  isAnimating   = false
  settleTimes   = number[]       // ms each char locks at
  startTime     = 0              // performance.now() at first tick
  prefersReduced = matchMedia query

helpers (pure, exported):
  pickScrambleChar(pool, rng) β†’ random character from pool
  computeSettleTimes(charCount, duration, order, rng) β†’ number[]
    'left-to-right': i_th time = ((i+1) / count) * duration
    'random':        i_th time = rng() * duration
  getDisplayString(text, settleTimes, elapsed, pool, rng) β†’ string
    for each char in text:
      if char is space β†’ keep space
      else if elapsed >= settleTimes[i] β†’ final char
      else β†’ pickScrambleChar(pool)
  isScrambleComplete(settleTimes, elapsed) β†’ bool

start animation:
  if prefersReduced: display = text; bail
  settleTimes = computeSettleTimes(text.length, duration, order)
  display = getDisplayString(text, settleTimes, 0, pool)  // paint a fully-scrambled frame first
  isAnimating = true
  if delay > 0: setTimeout(begin, delay) else begin()

  begin():
    startTime = performance.now()
    rafLoop(now):
      elapsed = now - startTime
      display = getDisplayString(text, settleTimes, elapsed, pool)
      if isScrambleComplete(settleTimes, elapsed):
        display = text; isAnimating = false; stop
      else:
        rafHandle = requestAnimationFrame(rafLoop)

on pointerenter:
  if replayOnHover and not isAnimating: startScramble()

on unmount:
  cancelAnimationFrame(rafHandle); clearTimeout(delayHandle)

The component itself is thin: build the settle-time array once, then run an rAF loop that calls a pure getDisplayString each frame.

The Core Concept: Per-Character Settle Times

The trick that gives the effect its rhythm is precomputing one timestamp per character β€” the settleTimes array. Each entry says "at this many milliseconds into the animation, this character stops scrambling and shows its final glyph". The rAF loop then becomes embarrassingly simple: read elapsed, ask each character whether its time has come.

text       = "DECODED"
duration   = 1500ms
order      = 'left-to-right'

i  char  settleTime          elapsed:  0    400   800   1200  1500
0  D     214  ms              ────────  β–‘     D     D     D     D
1  E     429  ms              ────────  β–’     β–’     E     E     E
2  C     643  ms              ────────  β–‘     β–“     C     C     C
3  O     857  ms              ────────  β–“     β–‘     β–’     O     O
4  D     1071 ms              ────────  β–’     β–’     β–‘     D     D
5  E     1286 ms              ────────  β–‘     β–“     β–’     β–‘     E
6  D     1500 ms              ────────  β–“     β–‘     β–’     β–“     D

  β–‘ β–’ β–“ = randomly picked glyphs from the pool at that frame

For 'random' order, settle times are uniform-random in [0, duration], which produces the chaotic "letters lock in unpredictable order" feel rather than the orderly left-to-right reveal.

The functions are split out as pure exports (pickScrambleChar, computeSettleTimes, getDisplayString, isScrambleComplete) so unit tests can pass a deterministic rng = () => 0 and assert the exact sequence without rendering anything.

Performance

  • One rAF loop while scrambling; cancelled the moment isScrambleComplete returns true.
  • The visible string updates a single text node β€” no per-letter <span> tree, so DOM cost stays flat regardless of text length.
  • No layout reads (getBoundingClientRect, offsetWidth, etc.) during the animation, so no forced reflows.
  • font-variant-numeric: tabular-nums and a hairline letter-spacing keep proportional fonts from "wobbling" as glyph widths change between frames.

CSS Animation Strategy

ScrambledText is JS-driven for the scramble itself (the random glyph pick has no CSS analogue), so the CSS layer stays minimal: a display: inline-block wrapper, tabular-nums for predictable digit width, and a 0.95 opacity nudge while is-animating to make the reveal feel slightly muted compared to the resting text. The settled string then snaps to full opacity, giving the eye a subtle "lock-in" moment without any explicit transition.

State Flow Diagram

  [idle]
     β”‚  autoStart on mount     replayOnHover pointerenter
     β–Ό
  [animating]  ── rAF tick ──┐
     β”‚                       β”‚
     β”‚  for each char:       β”‚
     β”‚   elapsed >= settle?  β”‚
     β”‚     yes β†’ final glyph β”‚
     β”‚     no  β†’ pick random β”‚
     β”‚                       β”‚
     β—€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
     β”‚
     β”‚  isScrambleComplete(elapsed) === true
     β–Ό
  [settled]   display = text; isAnimating = false

  prefers-reduced-motion: reduce
       └─ skip [animating], paint final text once.

Props Reference

Prop Type Default Description
text string required The final string to land on.
duration number 1500 Total scramble length in milliseconds.
pool string 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789' Pool of glyphs picked from while scrambling.
order 'left-to-right' | 'random' 'left-to-right' How per-character settle times are distributed.
replayOnHover boolean false Restart the scramble when the pointer enters the wrapper.
autoStart boolean true Run the scramble on mount; otherwise render the final text and wait for replayOnHover.
delay number 0 Milliseconds to wait after start is requested before the first rAF tick.
class string '' Extra classes on the wrapper span.

Edge Cases

Situation Behaviour
Empty text prop computeSettleTimes returns []; isScrambleComplete is true immediately and no rAF loop starts.
Single-character text settleTimes is [duration] regardless of order; the lone character locks in at the very end.
Empty pool pickScrambleChar returns ''; mid-scramble the character renders blank, then resolves to its final glyph at settle time.
User has prefers-reduced-motion: reduce display = text immediately; rAF never starts, no flicker.
replayOnHover triggered while still animating The hover handler bails out (if (!isAnimating)); current run completes before another can start.
Component unmounts mid-animation onMount teardown calls cancelTimers(), cancelling both the rAF handle and any pending setTimeout.
Spaces in text Spaces are passed through verbatim β€” they never scramble β€” so word boundaries remain readable as the rest of the string decodes.

Dependencies

  • Svelte 5.x β€” $state, $props, onMount for lifecycle.
  • Zero external dependencies β€” pure JS state machine + requestAnimationFrame.

File Structure

src/lib/components/ScrambledText.svelte   # implementation
src/lib/components/ScrambledText.md       # this file (rendered inside ComponentPageShell)
src/lib/components/ScrambledText.test.ts  # vitest unit tests for the pure helpers
src/routes/scrambledtext/+page.svelte     # demo page

API

04
PropTypeDefaultDescription
textstringrequiredFinal string to resolve to.
durationnumber1500Total settle time in ms.
poolstringA–Z 0–9Pool of glyphs to shuffle through.
order"left-to-right" | "random""left-to-right"Settle order.
autoStartbooleantrueRun on mount.
delaynumber0Delay before starting, ms.
replayOnHoverbooleanfalseRescramble on pointer enter.
classstring""Extra class for the wrapper span.