Kinetic typography is animated text

Kinetic typography is text that moves -- letters, words, or phrases that translate, scale, rotate, or reveal themselves over time to add meaning or emotion beyond static type. On the web, we build it by applying CSS transforms, opacity transitions, and clip-path reveals to HTML text elements, typically using libraries like GSAP and SplitType while keeping render performance at 60fps and respecting prefers-reduced-motion.


TL;DR: Split text into <span> elements with SplitType, animate those spans with GSAP or CSS using only transform and opacity, tie motion to scroll position with ScrollTrigger, and always provide a reduced-motion fallback. That's the entire recipe.



What is kinetic typography?

Kinetic typography means moving text. Letters, words, or phrases animate -- they translate across the viewport, scale up from zero, rotate into position, change color, or reveal through masks and clip paths. The technique dates back to Saul Bass's title sequence for North by Northwest in 1959. On the web in 2025, it means animating HTML text nodes with CSS and JavaScript rather than rendering video or canvas.

There's no single correct way to animate text, but on the web we have a constraint that After Effects doesn't: the browser's rendering pipeline. Every technique has to survive the compositor thread without dropping frames. That's what separates web kinetic typography from motion design tutorials -- we're not rendering to a video file, we're running at 60fps in real time on whatever device the user happens to be holding.

Static type vs. kinetic type

Property Static Typography Kinetic Typography
Attention capture Relies on size, weight, color Motion itself creates pattern interrupt
Emotional range Conveyed through typeface choice Conveyed through movement quality (speed, easing, direction)
Information pacing Reader controls pace Designer controls pace
Accessibility risk Low Medium to high (motion sensitivity)
Performance cost Zero Non-trivial (requires compositor-friendly properties)

Why does kinetic typography work on the web?

Motion draws the eye before anything else. On the web, kinetic typography works when it paces information rather than decorating it.

Consider a hero section where a headline fades in word by word as the page loads. Each word arrives in sequence, forcing the reader to process the message in your intended order. Compare that to a wall of text that appears instantly: the reader's eye might land on the third word, skip to the end, or bounce entirely.

Three specific reasons kinetic type works on the web:

  1. Pattern interrupt. A text element that translates upward 20px while fading in breaks the static rectangle pattern of typical page layouts.
  2. Information hierarchy through time. You can reveal a subheading 300ms after the headline, creating temporal hierarchy that reinforces visual hierarchy.
  3. Scroll engagement. Text that responds to scroll position gives users a sense of direct manipulation -- it feels like they're uncovering the content rather than passively reading.

None of these require complex tooling. All three can be built with CSS and a small amount of JavaScript.


How do you split text for animation?

You split text into individual <span> elements -- one per character, word, or line -- so each fragment can be independently animated. The standard tool is SplitType (v0.3.4 as of mid-2025), which wraps each character, word, and line in a <span> with a class you can target.

SplitType setup

import SplitType from 'split-type';

const text = new SplitType('#hero-heading', {
  types: 'words, chars',
  tagName: 'span'
});

// text.words => array of word <span> elements
// text.chars => array of character <span> elements

After SplitType runs, your markup goes from:

<h1 id="hero-heading">We build fast websites</h1>

To something like:

<h1 id="hero-heading">
  <span class="word"><span class="char">W</span><span class="char">e</span></span>
  <span class="word"><span class="char">b</span><span class="char">u</span>...</span>
  ...
</h1>

Now each character or word can receive its own transform, opacity, or clip-path animation.

Alternatives to SplitType

Library Size (minified) Notes
SplitType 0.3.4 ~4 KB Framework-agnostic, handles resize/revert
GSAP SplitText ~3 KB Part of GSAP Club/Business license ($99+/yr), tightly integrated
Splitting.js ~2 KB CSS-variable-based indexing, no JS animation API
Manual DOM manipulation 0 KB Fine for one-off elements, tedious at scale

We use SplitType on projects where the client doesn't have a GSAP Business license, and GSAP SplitText when they do. The SplitText plugin handles edge cases like nested elements and ligatures slightly better, but SplitType is free and solves 90% of use cases.


What CSS techniques produce kinetic type?

CSS alone can produce translate reveals, scale entrances, rotation, clip-path reveals, and color transitions. Here are the ones we ship in production.

1. Translate-and-fade entrance

Each word starts 20-40px below its final position with opacity: 0, then animates to its natural position.

.word {
  display: inline-block;
  opacity: 0;
  transform: translateY(30px);
  animation: revealUp 0.6s cubic-bezier(0.16, 1, 0.3, 1) forwards;
}

@keyframes revealUp {
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

/* Stagger each word by 80ms */
.word:nth-child(1) { animation-delay: 0ms; }
.word:nth-child(2) { animation-delay: 80ms; }
.word:nth-child(3) { animation-delay: 160ms; }
.word:nth-child(4) { animation-delay: 240ms; }

This is pure CSS, costs zero JavaScript, and runs entirely on the compositor because we only touch transform and opacity.

2. Clip-path text reveal

A clip-path reveal masks the text with clip-path: inset() and animates the inset values to zero, creating a wipe-in effect.

.reveal-line {
  clip-path: inset(0 100% 0 0);
  animation: clipReveal 0.8s cubic-bezier(0.77, 0, 0.175, 1) forwards;
}

@keyframes clipReveal {
  to {
    clip-path: inset(0 0% 0 0);
  }
}

Performance note: clip-path animation does not run on the compositor in all browsers. As of Chrome 128+, animating clip-path triggers paint on every frame. It looks great but costs more than transform/opacity. We use it sparingly -- on one or two hero elements, not on 40 list items.

3. CSS custom property stagger

Instead of writing an nth-child rule for each word, set a CSS custom property via the style attribute and reference it in animation-delay:

.word {
  display: inline-block;
  opacity: 0;
  transform: translateY(30px);
  animation: revealUp 0.6s cubic-bezier(0.16, 1, 0.3, 1) forwards;
  animation-delay: calc(var(--i, 0) * 80ms);
}

Then in JS (or via SplitType's output), set --i on each word:

document.querySelectorAll('.word').forEach((el, i) => {
  el.style.setProperty('--i', i);
});

Splitting.js does this automatically -- it sets --char-index and --word-index on each element, which is why it's popular for CSS-only kinetic type.


How do you animate text with GSAP?

GSAP (GreenSock Animation Platform, v3.12) is the standard library for complex kinetic typography on the web. It gives you precise timeline control, physics-based easing, and -- critically -- it automatically uses transform and opacity for DOM animations, keeping you on the compositor.

Basic staggered word reveal

import { gsap } from 'gsap';
import SplitType from 'split-type';

const split = new SplitType('h1', { types: 'words' });

gsap.from(split.words, {
  y: 40,
  opacity: 0,
  duration: 0.7,
  ease: 'power3.out',
  stagger: 0.06,
});

Seven lines for a production-quality headline animation. The stagger: 0.06 means each word starts 60ms after the previous one.

Character-level rotation

const split = new SplitType('h1', { types: 'chars' });

gsap.from(split.chars, {
  rotationX: -90,
  opacity: 0,
  duration: 0.8,
  ease: 'back.out(1.7)',
  stagger: 0.03,
  transformOrigin: 'bottom center',
});

Each character flips in from below, like cards flipping on a departure board. The transformOrigin is important -- without it, the rotation axis defaults to center, which looks wrong for text.

Timeline for sequenced reveals

When you need a heading to animate in, then a subheading, then a CTA button, use a GSAP timeline:

const tl = gsap.timeline({ defaults: { ease: 'power3.out' } });

tl.from(headingSplit.words, { y: 40, opacity: 0, stagger: 0.05, duration: 0.6 })
  .from(subheadingSplit.words, { y: 20, opacity: 0, stagger: 0.04, duration: 0.5 }, '-=0.3')
  .from('.cta-button', { y: 20, opacity: 0, duration: 0.4 }, '-=0.2');

The '-=0.3' overlap means the subheading starts 300ms before the heading animation finishes. This overlap is what makes kinetic type feel fluid rather than robotic.


Scroll-linked kinetic typography ties animation progress to the user's scroll position rather than playing on a timer. GSAP ScrollTrigger (v3.12) is the tool we use. The newer CSS animation-timeline: scroll() spec is promising but still lacks Safari support as of June 2025.

GSAP ScrollTrigger example

import { gsap } from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';
import SplitType from 'split-type';

gsap.registerPlugin(ScrollTrigger);

const split = new SplitType('.scroll-text', { types: 'words' });

gsap.from(split.words, {
  y: 30,
  opacity: 0,
  stagger: 0.05,
  scrollTrigger: {
    trigger: '.scroll-text',
    start: 'top 80%',
    end: 'top 40%',
    scrub: true,
  },
});

With scrub: true, animation progress maps directly to scroll position. Scroll down -- text reveals. Scroll back up -- text hides. This creates the "text unfurling as you scroll" effect you see on Awwwards-winning sites.

CSS Scroll-Driven Animations (spec level)

Chrome 115+ and Edge 115+ support animation-timeline: scroll(). Firefox has partial support behind a flag. Safari has no support yet.

.scroll-word {
  opacity: 0;
  transform: translateY(30px);
  animation: revealUp linear both;
  animation-timeline: scroll(nearest block);
  animation-range: entry 0% entry 50%;
}

This is zero-JS scroll-linked animation. We've shipped it with a GSAP fallback for Safari.


What is the transform-and-opacity-only rule?

The rule: if you want 60fps animation, only animate transform and opacity. These two properties can be handled entirely by the browser's compositor thread, which means they don't trigger layout or paint.

Animations that trigger layout (like animating width, height, top, left, margin, or padding) force the browser to recalculate geometry for the entire page on every frame. Animations that trigger paint (like background-color, box-shadow, clip-path, border-radius) are cheaper than layout but still run on the main thread.

What you can animate at 60fps

Property Compositor-only? Safe for kinetic type?
transform: translateX/Y/Z() Yes Yes
transform: scale() Yes Yes
transform: rotate() Yes Yes
opacity Yes Yes
clip-path No (triggers paint) Use sparingly
color No (triggers paint) Use sparingly
filter: blur() Depends on browser Test per target
width / height No (triggers layout) Never
top / left No (triggers layout) Never

Practical translation for kinetic type

  • Move text? Use transform: translate(), not top/left/margin.
  • Scale text? Use transform: scale(), not font-size animation.
  • Fade text? Use opacity.
  • Reveal text? Use clip-path if you accept the paint cost, or use an overflow-hidden parent with a translateY child (compositor-only).
  • Change text color? Animate a pseudo-element's opacity on top, rather than animating color directly.

The overflow-hidden + translateY pattern is our go-to for text reveals:

.line-wrapper {
  overflow: hidden;
}

.line-inner {
  transform: translateY(100%);
  animation: slideUp 0.6s cubic-bezier(0.16, 1, 0.3, 1) forwards;
}

@keyframes slideUp {
  to { transform: translateY(0); }
}

The text slides up from behind the wrapper. No clip-path, no paint, pure compositor. This is what most Awwwards Site of the Day winners use for their heading reveals -- it runs at a locked 60fps on a $200 Android phone.


How do you handle prefers-reduced-motion?

prefers-reduced-motion is a CSS media query that reports whether the user has requested minimal animation in their OS settings. On macOS it's System Settings > Accessibility > Display > Reduce motion. On iOS it's Settings > Accessibility > Motion > Reduce Motion. On Windows it's Settings > Accessibility > Visual effects > Animation effects.

You must respect it. This is a WCAG 2.1 Level AAA success criterion (2.3.3), and at Level A, WCAG 2.3.1 requires that nothing flashes more than three times per second.

CSS approach

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

This nuclear option kills all animation site-wide. We prefer a more targeted approach -- remove motion but keep opacity fades, since opacity changes don't cause vestibular discomfort:

@media (prefers-reduced-motion: reduce) {
  .word, .char, .line-inner {
    transform: none !important;
    animation: none !important;
  }
  
  .word {
    opacity: 1 !important;
  }
}

JavaScript approach (GSAP)

const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;

if (!prefersReducedMotion) {
  gsap.from(split.words, { y: 40, opacity: 0, stagger: 0.06 });
} else {
  gsap.set(split.words, { y: 0, opacity: 1 });
}

We wrap every animation initialization in this check. It costs one line of code and respects the user's explicit preference.


Performance and accessibility caveats

Performance caveats

  1. DOM element count matters. Splitting a 200-word paragraph into individual character spans creates 1,000+ DOM elements. Animating all of them simultaneously will stall even a fast device. Split only the text you're actually animating -- headlines, pull quotes, CTAs. Not body copy.

  2. will-change is not free. Adding will-change: transform promotes an element to its own compositor layer. Each layer consumes GPU memory. On a page with 300 animated character spans, that's 300 layers. We've seen this cause visible jank on iPhones with 3GB RAM. Use will-change only on elements currently animating, and remove it after.

  3. Reflow on resize. SplitType's split depends on the element's rendered width. When the viewport resizes, line breaks change, and the split becomes invalid. Call split.revert() on resize, debounce 200ms, then re-split. SplitType handles this if you use its built-in resize observer, but verify it's working.

  4. Font loading. If your web font hasn't loaded when SplitType runs, the split will be calculated against the fallback font's metrics. Use document.fonts.ready before splitting:

await document.fonts.ready;
const split = new SplitType('h1', { types: 'words' });

Accessibility caveats

  1. Screen readers and split text. When SplitType wraps each character in a <span>, some screen readers may announce individual characters instead of words. Add aria-label with the full text on the parent element and aria-hidden="true" on the split spans:
<h1 aria-label="We build fast websites">
  <span aria-hidden="true">
    <!-- split spans here -->
  </span>
</h1>
  1. Motion duration. Animations should complete within 500ms for entrance animations and 300ms for micro-interactions. Anything longer than 1 second starts to feel like it's blocking the user from reading.

  2. No animation on text the user needs to read immediately. Error messages, form validation, navigation labels -- these should never be kinetically animated. Reserve kinetic type for editorial/marketing content where a 400ms delay is acceptable.

  3. Pause controls. If you have auto-playing kinetic type (like a rotating word in a headline), provide a visible pause button. WCAG 2.2.2 requires this for any animation that starts automatically, lasts more than 5 seconds, and is presented alongside other content.


Real-world examples worth studying

  1. Apple product pages (apple.com/iphone) -- scroll-linked text reveals using the overflow-hidden + translateY technique. Each line slides up as you scroll. No clip-path, pure compositor animations.

  2. Awwwards Site of the Day winners -- browse awwwards.com/websites/sites_of_the_day and inspect the heading animations. The majority use GSAP + SplitText/SplitType. You'll see the same y: 30, opacity: 0, stagger: 0.05 pattern we described above.

  3. Studio Freight's Lenis + GSAP combo -- their open-source smooth scroll library (Lenis) paired with GSAP ScrollTrigger is the most common stack for scroll-linked kinetic type in agency work right now.

  4. Saul Bass's North by Northwest (1959) title sequence -- the credits translating in from off-screen edges is literally the same translateX pattern we use in CSS today, just executed with optical printing instead of a browser.

For projects where you need kinetic typography integrated with scroll-driven storytelling, parallax layers, and performant page transitions, our immersive website design and scroll animation work covers exactly that stack.


FAQ

What is kinetic typography in web design?

Kinetic typography in web design is the animation of HTML text elements using CSS transforms, opacity transitions, clip-path reveals, and JavaScript animation libraries like GSAP. It makes headlines, pull quotes, and CTAs move, rotate, scale, or reveal on page load or scroll.

Is GSAP free for kinetic typography?

GSAP's core library and ScrollTrigger plugin are free for all projects, including commercial ones, as of v3.12. The SplitText plugin requires a GSAP Club or Business license starting at $99/year. SplitType is a free open-source alternative.

Does kinetic typography hurt SEO?

No, because the text remains in the DOM as real HTML. Search engines read the text content regardless of CSS transforms or opacity values. Avoid rendering text in <canvas> or SVG paths -- those are invisible to search crawlers.

How do I make kinetic typography accessible?

Respect prefers-reduced-motion by disabling or reducing animations when the media query matches. Add aria-label to parent elements when text is split into character spans. Keep animation durations under 500ms for entrances.

What's the difference between kinetic typography and motion graphics?

Kinetic typography specifically refers to animated text. Motion graphics is the broader category that includes animated illustrations, icons, data visualizations, and any non-video animated content. Kinetic typography is a subset of motion graphics.

Can I do kinetic typography without JavaScript?

Yes -- CSS @keyframes, animation-delay with custom properties, and the newer animation-timeline: scroll() spec can produce staggered text reveals, clip-path wipes, and scroll-linked animations without any JS. Browser support for scroll-driven animations is the main limitation as of mid-2025.

What font properties should I avoid animating?

Never animate font-size, letter-spacing, line-height, or word-spacing -- these trigger layout recalculation on every frame and will drop you well below 60fps. Use transform: scale() instead of font-size for size changes.