14 CSS Typewriter Effect Designs 12 / 14

CSS Typewriter Glitch on Type

Each character causes a brief glitch distortion on the preceding text as it lands — RGB channel splitting and clip-path slice distortion fire per-keystroke via JS class toggling.

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

The code

<div class="tw-12">
  <div class="tw-12__stage">
    <div class="tw-12__chip">SYSTEM BREACH</div>
    <div class="tw-12__wrap">
      <div class="tw-12__text" id="tw-12-text" data-text=""></div>
    </div>
    <div class="tw-12__controls">
      <button id="tw-12-restart" class="tw-12__btn">↺ REINITIALISE</button>
    </div>
  </div>
</div>
.tw-12, .tw-12 *, .tw-12 *::before, .tw-12 *::after { box-sizing: border-box; margin: 0; padding: 0; }
.tw-12 ::selection { background: #ef4444; color: #1a0000; }

.tw-12 {
  --red: #ef4444;
  --cyan: #22d3ee;
  --bg: #0a0000;
  --text: #f8fafc;
  font-family: 'Courier New', monospace;
  min-height: 340px;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 48px 24px;
}

.tw-12__stage {
  width: 100%;
  max-width: 500px;
  display: flex;
  flex-direction: column;
  gap: 20px;
}

.tw-12__chip {
  font-size: 0.68rem;
  letter-spacing: 0.25em;
  color: var(--red);
  border: 1px solid rgba(239,68,68,0.3);
  display: inline-block;
  padding: 4px 10px;
  border-radius: 3px;
}

.tw-12__wrap {
  position: relative;
  overflow: hidden;
  min-height: 3.5rem;
}

.tw-12__text {
  font-size: clamp(1.4rem, 4.5vw, 2.2rem);
  font-weight: 700;
  color: var(--text);
  line-height: 1.3;
  position: relative;
  white-space: pre-wrap;
  word-break: break-word;
}

.tw-12__text::before,
.tw-12__text::after {
  content: attr(data-text);
  position: absolute;
  inset: 0;
  overflow: hidden;
  opacity: 0;
  pointer-events: none;
}

.tw-12__text.glitching::before {
  opacity: 0.8;
  color: var(--red);
  transform: translateX(-4px);
  clip-path: inset(var(--ct, 20%) 0 var(--cb, 60%) 0);
}

.tw-12__text.glitching::after {
  opacity: 0.8;
  color: var(--cyan);
  transform: translateX(4px);
  clip-path: inset(var(--ct2, 50%) 0 var(--cb2, 30%) 0);
}

.tw-12__controls {
  display: flex;
  gap: 12px;
}

.tw-12__btn {
  background: transparent;
  border: 1px solid rgba(239,68,68,0.3);
  color: var(--red);
  font-family: inherit;
  font-size: 0.75rem;
  letter-spacing: 0.12em;
  padding: 7px 16px;
  cursor: pointer;
  border-radius: 4px;
  transition: border-color 0.2s;
}
.tw-12__btn:hover { border-color: var(--red); }

@media (prefers-reduced-motion: reduce) {
  .tw-12__text.glitching::before,
  .tw-12__text.glitching::after { opacity: 0; }
}
(function() {
  const el = document.getElementById('tw-12-text');
  const btn = document.getElementById('tw-12-restart');
  if (!el) return;

  const LINES = [
    'Firewall bypassed.',
    'Injecting payload...',
    'Root access obtained.'
  ];

  let timer = null;
  let lineIdx = 0;
  let charIdx = 0;

  function glitch() {
    const t1 = Math.random() * 40;
    const t2 = t1 + 10 + Math.random() * 20;
    el.style.setProperty('--ct', t1 + '%');
    el.style.setProperty('--cb', (100 - t2) + '%');
    el.style.setProperty('--ct2', (t1 + 30) % 80 + '%');
    el.style.setProperty('--cb2', (100 - t2 - 10) + '%');
    el.classList.add('glitching');
    setTimeout(() => el.classList.remove('glitching'), 80);
  }

  function typeNext() {
    const line = LINES[lineIdx];
    if (charIdx <= line.length) {
      const current = line.slice(0, charIdx);
      el.textContent = current;
      el.dataset.text = current;
      if (charIdx > 0) glitch();
      charIdx++;
      timer = setTimeout(typeNext, 70 + Math.random() * 40);
    } else {
      timer = setTimeout(() => {
        lineIdx = (lineIdx + 1) % LINES.length;
        charIdx = 0;
        timer = setTimeout(typeNext, 300);
      }, 1800);
    }
  }

  function restart() {
    clearTimeout(timer);
    el.textContent = '';
    el.dataset.text = '';
    el.classList.remove('glitching');
    lineIdx = 0; charIdx = 0;
    timer = setTimeout(typeNext, 400);
  }

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

How this works

JS types one character per interval tick and simultaneously toggles a .glitching class on the output element for 80ms before removing it. In CSS, .glitching::before and .glitching::after pseudo-elements duplicate the text via content: attr(data-text), offset by ±4px on the X axis, and tinted to red and cyan channel colours. A clip-path: inset() on each pseudo clips a random horizontal slice, making the glitch look like a scan-line artifact rather than a full duplicate.

The clip-path values are randomised on each glitch trigger by JS setting CSS custom properties — --clip-top and --clip-h as percentages. This gives every keystroke a unique glitch signature rather than a repeating animation loop, making the effect feel reactive to input.

Customize

  • Increase glitch intensity by widening the X offset on pseudo-elements from 4px to 8px and extending the glitch class duration from 80ms to 150ms.
  • Add a third pseudo-element with a green channel offset for a full RGB-split effect — though this requires CSS variables on ::marker which needs a different DOM strategy.
  • Trigger glitch not on each character but only on punctuation or space characters for a subtler effect: "glitches at word boundaries" rather than every keystroke.

Watch out for

  • content: attr(data-text) on pseudo-elements only mirrors plain text — if the original element contains child HTML tags, the pseudo-element won't replicate them.
  • The glitch pseudo-elements must use position: absolute and the parent must be position: relative; overflow: hidden to prevent the offset copies from bleeding outside the container.
  • On Safari, clip-path: inset() values set via CSS custom properties inside pseudo-elements may not update dynamically — use a class swap strategy with pre-defined clip values instead.

Browser support

ChromeSafariFirefoxEdge
55+ 13.1+ 54+ 55+

clip-path on pseudo-elements is supported in all modern browsers. CSS custom properties in clip-path requires Safari 13.1+.

Search CodeFronts

Loading…