Kinetic typography

Kinetic typography

bold

Word-by-word reveal that lands like a punchline.

Split a headline into individual words, animate them up from below with staggered timing. The italic accent word gets a slightly longer delay so it lands last — the rhythm makes the headline feel earned.

animatedtextherorevealgsap

━━ Use this pattern

CDN deps
cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/gsap.min.js

━━ Live preview

━━ HTML markup

<h1 class="kt-headline">
  <span class="kt-word">The</span>
  <span class="kt-word">design</span>
  <span class="kt-word">system</span>
  <span class="kt-word it">behind</span>
  <span class="kt-word">every</span>
  <span class="kt-word">page.</span>
</h1>

━━ CSS

.kt-headline {
  font-family: "Geist", system-ui, sans-serif;
  font-size: clamp(2.5rem, 8vw, 6rem);
  font-weight: 700;
  letter-spacing: -0.04em;
  line-height: 0.98;
  /* stageMode "adaptive": type only, so it takes the surface's own ink. */
  --kt-ink: var(--color-on-surface, #fafafa);
  /* The italic word is the headline's one saturated mark; it mixes toward the surface
     first so a dark or cool primary still lands as emphasis rather than as a hole. */
  --kt-accent: color-mix(in oklab, var(--color-primary, #ff6b1a) 80%, var(--color-surface, #ff6b1a));
  color: var(--kt-ink);
  overflow: hidden;
  padding: 0.1em 0;
}
.kt-word {
  display: inline-block;
  opacity: 0;
  transform: translateY(60%);
  margin-right: 0.25em;
  transition: opacity 700ms cubic-bezier(0.16,1,0.3,1), transform 700ms cubic-bezier(0.16,1,0.3,1);
}
.kt-word.kt-shown { opacity: 1; transform: none; }
.kt-word.it {
  font-family: "Instrument Serif", serif;
  font-style: italic;
  font-weight: 400;
  color: var(--kt-accent);
}

━━ JavaScript

(function() {
  var words = Array.prototype.slice.call(document.querySelectorAll('.kt-word'));
  if (!words.length) return;
  var reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;

  function hideAll() { words.forEach(function(w){ w.classList.remove('kt-shown'); w.style.opacity=''; w.style.transform=''; }); }
  function showAllInstant() { words.forEach(function(w){ w.classList.add('kt-shown'); }); }

  function revealCss() {
    // Staggered CSS reveal (no GSAP needed).
    words.forEach(function(w, i){ setTimeout(function(){ w.classList.add('kt-shown'); }, 200 + i * 120); });
  }
  function revealGsap() {
    try {
      window.gsap.set('.kt-word', { clearProps: 'opacity,transform' });
      window.gsap.fromTo('.kt-word',
        { opacity: 0, y: '60%' },
        { opacity: 1, y: 0, duration: 1, ease: 'power3.out', stagger: 0.12, delay: 0.2 });
      words.forEach(function(w){ w.classList.add('kt-shown'); });
    } catch (e) { revealCss(); }
  }

  function reveal() {
    if (reduce) { showAllInstant(); return; }
    if (typeof window.gsap !== 'undefined' && window.gsap.fromTo) revealGsap();
    else revealCss();
  }

  // Wait briefly for GSAP; if it never arrives, reveal via CSS so the headline is
  // NEVER left invisible (production safety).
  var gsapTries = 0;
  function whenReady(cb) {
    if (reduce) { cb(); return; }
    if (typeof window.gsap !== 'undefined') { cb(); return; }
    if (gsapTries++ > 25) { cb(); return; }   // ~2.5s max, then proceed (CSS fallback)
    setTimeout(function(){ whenReady(cb); }, 100);
  }

  var played = false;
  function playOnce() {
    if (played) return; played = true;
    whenReady(reveal);
    /* cp:demo-start */startDemoLoopIfNeeded();/* cp:demo-end */
  }

  /* cp:demo-start */
  function startDemoLoopIfNeeded() {
    if (!window.__CP_DEMO__ || reduce) return;
    setInterval(function(){
      if (window.__cpPaused) return;
      hideAll();
      setTimeout(reveal, 250);
    }, 3800);
  }
  /* cp:demo-end */

  // Trigger on visibility, with a load fallback.
  var triggered = false;
  function fire(){ if (triggered) return; triggered = true; playOnce(); }
  try {
    var io = new IntersectionObserver(function(entries){
      entries.forEach(function(e){ if (e.isIntersecting) fire(); });
    }, { threshold: 0.2 });
    var h = document.querySelector('.kt-headline'); if (h) io.observe(h);
  } catch (e) {}
  setTimeout(fire, 500);

  // Absolute safety net: no matter what, the headline is visible within 3s.
  if (!reduce) setTimeout(function(){ if (!played) { showAllInstant(); } }, 3000);
})();
Kinetic typography · CreativePower