25 CSS Text Animations 23 / 25

CSS Cursor Typewriter Blink Animation

A JavaScript-driven typewriter that cycles through multiple phrases with cursor blink, delete speed, and per-phrase pause — a classic hero animation.

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

The code

<div class="ta-23">
  <div class="ta-23__stage">
    <p class="ta-23__prefix">We design</p>
    <div class="ta-23__line">
      <span class="ta-23__typed" id="ta-23-typed"></span><span class="ta-23__cursor" id="ta-23-cursor">▋</span>
    </div>
  </div>
</div>
.ta-23, .ta-23 *, .ta-23 *::before, .ta-23 *::after {
  margin: 0; padding: 0; box-sizing: border-box;
}
.ta-23 ::selection { background: #7c3aed; color: #fff; }

.ta-23 {
  --bg: #f5f3ff;
  min-height: 100vh;
  background: var(--bg);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 2rem;
  font-family: 'Plus Jakarta Sans', 'Segoe UI', sans-serif;
}

.ta-23__stage { text-align: left; }

.ta-23__prefix {
  font-size: clamp(0.9rem, 2.5vw, 1.2rem);
  color: #a78bfa;
  font-weight: 600;
  letter-spacing: 0.05em;
  margin-bottom: 0.2rem;
}

.ta-23__line {
  display: flex;
  align-items: center;
  min-height: 1.2em;
}

.ta-23__typed {
  font-size: clamp(2rem, 6vw, 3.8rem);
  font-weight: 800;
  color: #1e1b4b;
  white-space: nowrap;
}

.ta-23__cursor {
  font-size: clamp(2rem, 6vw, 3.8rem);
  font-weight: 400;
  color: #7c3aed;
  animation: ta-23-blink 0.85s steps(1) infinite;
  margin-left: 2px;
  line-height: 1;
}

.ta-23__cursor.typing {
  animation-play-state: paused;
}

@keyframes ta-23-blink {
  0%, 100% { opacity: 1; }
  50%       { opacity: 0; }
}

@media (prefers-reduced-motion: reduce) {
  .ta-23__cursor { animation: none; opacity: 1; }
}
(function() {
  const typedEl  = document.getElementById('ta-23-typed');
  const cursorEl = document.getElementById('ta-23-cursor');
  if (!typedEl || !cursorEl) return;

  const phrases       = ['Experiences', 'Systems', 'Interfaces', 'Futures', 'Stories'];
  const typeSpeed     = 75;
  const deleteSpeed   = 38;
  const pauseDuration = 1600;

  let phraseIdx = 0;
  let charIdx   = 0;
  let deleting  = false;

  function tick() {
    const current = phrases[phraseIdx];

    if (!deleting) {
      charIdx++;
      typedEl.textContent = current.slice(0, charIdx);
      cursorEl.classList.add('typing');

      if (charIdx === current.length) {
        cursorEl.classList.remove('typing');
        setTimeout(() => { deleting = true; tick(); }, pauseDuration);
        return;
      }
      setTimeout(tick, typeSpeed);
    } else {
      charIdx--;
      typedEl.textContent = current.slice(0, charIdx);
      cursorEl.classList.add('typing');

      if (charIdx === 0) {
        cursorEl.classList.remove('typing');
        deleting = false;
        phraseIdx = (phraseIdx + 1) % phrases.length;
        setTimeout(tick, 300);
        return;
      }
      setTimeout(tick, deleteSpeed);
    }
  }

  tick();
})();

How this works

JavaScript maintains a state machine with four phases: typing (adding one character every typeSpeed ms using setTimeout), pause (holding the full word for a configurable delay), deleting (removing one character every deleteSpeed ms), and next (advancing the phrase index and transitioning back to typing). Each phase reschedules itself recursively, creating a continuous loop.

The blinking cursor is a span with a CSS animation (ta-23-blink) that toggles opacity between 1 and 0 using steps(1) timing for a hard digital blink. During the typing and deleting phases the cursor blink is paused via animation-play-state: paused to simulate a real terminal cursor that stops blinking while typing is active, and resumes during the pause phase.

Customize

  • Edit the phrases array to change what text is typed — any number of strings, any length, including spaces, punctuation, and emojis.
  • Adjust typeSpeed and deleteSpeed — slower type with faster delete (80ms / 35ms) creates a satisfying asymmetric rhythm.
  • Change pauseDuration to control how long each complete phrase is displayed before deleting — 1200ms is quick, 3000ms lets it be read.
  • Style the cursor by changing its background or switching from a block cursor to a blinking underline using border-bottom instead.
  • Add a callback when each phrase resolves to trigger other animations — like a CTA button that fades in only when the typing finishes for dramatic effect.

Watch out for

  • Using setTimeout recursively is more reliable than setInterval for typewriter effects because each callback is scheduled after the previous one completes, preventing drift.
  • The cursor element must always remain in the DOM even while typing — append text as adjacent text nodes or a sibling span, never innerHTML the cursor element itself.
  • Pause the animation when the tab is hidden using the Page Visibility API (document.visibilityState) to prevent the timer from drifting when the browser deprioritizes background tabs.

Browser support

ChromeSafariFirefoxEdge
All All All All

Uses only standard setTimeout and DOM text nodes. CSS blink animation is universally supported. Works in all modern browsers.

Search CodeFronts

Loading…