14 CSS Typewriter Effect Designs 11 / 14

CSS Typewriter Matrix Scramble Decode

Random characters scramble across the target string before each letter snaps to its final value — a hacker-movie decode effect powered by JS with CSS glow styling.

CSS + JS MIT licensed
Live Demo Open in tab
Open in playground

The code

<div class="tw-11">
  <div class="tw-11__screen">
    <div class="tw-11__scan"></div>
    <p class="tw-11__status">DECRYPTING PAYLOAD</p>
    <div class="tw-11__output" id="tw-11-output" aria-label="ACCESS GRANTED" aria-live="off"></div>
    <button class="tw-11__btn" id="tw-11-run">▶ DECODE</button>
  </div>
</div>
.tw-11, .tw-11 *, .tw-11 *::before, .tw-11 *::after { box-sizing: border-box; margin: 0; padding: 0; }
.tw-11 ::selection { background: #00ff41; color: #000; }

.tw-11 {
  --green: #00ff41;
  --dim: #003d10;
  --bg: #020a03;
  font-family: 'Courier New', monospace;
  min-height: 340px;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 40px 24px;
}

.tw-11__screen {
  width: 100%;
  max-width: 480px;
  border: 1px solid #0a2a0a;
  border-radius: 8px;
  padding: 32px 24px;
  position: relative;
  overflow: hidden;
  background: #030c03;
  box-shadow: 0 0 40px rgba(0,255,65,0.08), inset 0 0 40px rgba(0,0,0,0.5);
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 20px;
}

.tw-11__scan {
  position: absolute;
  inset: 0;
  background: repeating-linear-gradient(
    0deg, transparent, transparent 2px, rgba(0,255,65,0.03) 2px, rgba(0,255,65,0.03) 4px
  );
  pointer-events: none;
}

.tw-11__status {
  font-size: 0.7rem;
  letter-spacing: 0.2em;
  color: var(--dim);
}

.tw-11__output {
  font-size: clamp(1.4rem, 4vw, 2rem);
  font-weight: 700;
  color: var(--green);
  text-shadow: 0 0 10px rgba(0,255,65,0.6);
  letter-spacing: 0.08em;
  min-height: 1.4em;
  display: flex;
  flex-wrap: wrap;
  gap: 0;
}

.tw-11__output .tw-11-char {
  display: inline-block;
  color: var(--dim);
  text-shadow: none;
  transition: color 0.1s, text-shadow 0.1s;
}
.tw-11__output .tw-11-char.locked {
  color: var(--green);
  text-shadow: 0 0 12px rgba(0,255,65,0.8);
}

.tw-11__btn {
  background: transparent;
  border: 1px solid var(--dim);
  color: var(--green);
  font-family: inherit;
  font-size: 0.78rem;
  letter-spacing: 0.15em;
  padding: 8px 18px;
  cursor: pointer;
  border-radius: 4px;
  transition: border-color 0.2s, box-shadow 0.2s;
}
.tw-11__btn:hover {
  border-color: var(--green);
  box-shadow: 0 0 12px rgba(0,255,65,0.2);
}

@media (prefers-reduced-motion: reduce) {
  .tw-11__output .tw-11-char { transition: none; }
}
(function() {
  const container = document.getElementById('tw-11-output');
  const btn = document.getElementById('tw-11-run');
  if (!container) return;

  const TARGET = 'ACCESS GRANTED';
  const NOISE = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*<>?/|';
  const SCRAMBLE_FRAMES = 10;
  const CHAR_DELAY = 60;

  let intervals = [];

  function rand(str) {
    return str[Math.floor(Math.random() * str.length)];
  }

  function run() {
    intervals.forEach(clearInterval);
    intervals = [];
    container.innerHTML = '';

    const spans = TARGET.split('').map((ch) => {
      const span = document.createElement('span');
      span.className = 'tw-11-char';
      span.textContent = ch === ' ' ? ' ' : rand(NOISE);
      container.appendChild(span);
      return { span, final: ch };
    });

    spans.forEach(({ span, final }, i) => {
      if (final === ' ') { span.textContent = ' '; return; }
      let frames = 0;
      const startDelay = i * CHAR_DELAY;
      setTimeout(() => {
        const iv = setInterval(() => {
          if (frames >= SCRAMBLE_FRAMES) {
            clearInterval(iv);
            span.textContent = final;
            span.classList.add('locked');
          } else {
            span.textContent = rand(NOISE);
            frames++;
          }
        }, 40);
        intervals.push(iv);
      }, startDelay);
    });
  }

  btn.addEventListener('click', run);
  run();
})();

How this works

JS splits the target string into character slots. For each slot, a rapid setInterval replaces the character with a random glyph from a noise alphabet (Latin, symbols, Katakana) for a configurable number of iterations. After those scramble frames complete, the interval is cleared and the correct character is written. Each slot starts its scramble on a staggered setTimeout proportional to its index, creating a left-to-right cascade decode.

Every character slot is a <span> with a CSS custom property that the JS sets to a value between 0 and 1 (progress). CSS maps this progress to a color lerp from dim-cyan to bright-white using color-mix(), and the text-shadow intensity scales accordingly — so scrambling characters glow dimly and locked characters glow brightly.

Customize

  • Swap the noise alphabet from Latin symbols to Katakana only (const CHARS = "アイウエオカキクケコ...") for a more cinematic Matrix-style decode.
  • Control scramble intensity by increasing scrambleFrames from 8 to 20 — longer scrambles feel more "encrypted", shorter ones feel like a fast autofill.
  • Trigger the decode on IntersectionObserver entry rather than page load for a scroll-activated reveal on long landing pages.

Watch out for

  • Setting too many concurrent setInterval calls (one per character for long strings) can cause jank — batch character slots into groups of 4–6 and share a single interval.
  • Random character width variance causes layout shift if the container is not fixed-width — use a monospace font and fixed ch-based width on each character span.
  • Screen readers will read out the scrambled characters as they change — add aria-hidden="true" on the scramble container and provide an aria-label with the final decoded text.

Browser support

ChromeSafariFirefoxEdge
111+ 16.2+ 113+ 111+

color-mix() requires Chrome 111+, Safari 16.2+, Firefox 113+. Fallback: use static hex colour for older browsers.

Search CodeFronts

Loading…