---
name: kinetic-typography
description: Use when you want "Bold, editorial, expressive" - Uses animated typography as a central visual element.
---

# Kinetic Typography

> **Category:** Motion / Interaction  -  **Personality:** Bold, editorial, expressive
>
> **Best use:** Hero sections, campaigns, videos
>
> **Ratings:** Professional 3/5 - Casual 4/5 - Exotic 5/5 - Visual impact 5/5 - Difficulty 4/5 - Performance cost 3/5

## What it is

Kinetic typography makes the *type itself* the moving picture: words swap, scale, flip and slide on a choreographed loop so the headline reads like a title card from a brand film. Here the centrepiece is a **masked word-rotator** — one oversized verb cycles `design → build → animate → ship` inside a clipped slot, each entering with a 3D flip and a colour of its own, framed by drifting outline words, a live timecode and film grain. Reach for it on hero sections, campaign landing pages and video-style intros where you want maximum expressive impact from text alone. It is pure CSS (the motion needs no JavaScript), which keeps it cheap and resilient.

## Dependencies / CDN

**None — pure CSS** for the effect. The demo only loads three display/UI fonts (optional, swap for your own):

```html
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Anton&family=Playfair+Display:ital,wght@1,700;1,800;1,900&family=Space+Mono:ital,wght@0,400;0,700;1,400&display=swap" rel="stylesheet">
```

`Anton` = condensed poster caps, `Playfair Display` italic = the expressive rotating word, `Space Mono` = the film-UI labels.

## HTML

The whole effect is the rotor: a fixed-height, `overflow:hidden` slot with the words stacked inside it. Each word carries its index (`--i`) and accent colour (`--accent`) as inline custom properties.

```html
<h2 class="kt-lockup">
  <!-- one accessible sentence for screen readers; visual parts are decorative -->
  <span class="kt-sr">We design, build, animate and ship brands in motion.</span>

  <span class="kt-we" aria-hidden="true">We</span>

  <span class="kt-rotor" aria-hidden="true">
    <span class="kt-word kt-word--lead" style="--i:0;--accent:#c8ff3d">design</span>
    <span class="kt-word"             style="--i:1;--accent:#ff5247">build</span>
    <span class="kt-word"             style="--i:2;--accent:#8b7bff">animate</span>
    <span class="kt-word"             style="--i:3;--accent:#36e0d6">ship</span>
    <span class="kt-underline"></span>
  </span>

  <span class="kt-tail" aria-hidden="true">Brands in motion<span class="kt-dot">.</span></span>
</h2>
```

## CSS

```css
/* The clipped stage. height in `em` so it tracks the font size; perspective enables
   the 3D flip. clip-path:inset(0 -1000px) masks ONLY top/bottom — so a word longer
   than the box (e.g. "animate") overflows sideways instead of being chopped. */
.kt-rotor{
  position:relative; display:block; width:100%; height:1.25em;
  clip-path:inset(0 -1000px);
  perspective:900px;
  font-family:'Playfair Display',serif; font-style:italic; font-weight:800;
  font-size:clamp(56px,14vw,160px); line-height:1;
}

/* Every word is stacked on top of the others and runs the SAME 8s loop... */
.kt-word{
  position:absolute; inset:0;
  display:flex; align-items:center; justify-content:center;
  color:var(--accent); opacity:0; backface-visibility:hidden;
  will-change:transform,opacity,filter;
  text-shadow:0 0 38px color-mix(in srgb, var(--accent) 38%, transparent);
  animation:kt-cycle 8s cubic-bezier(.2,.62,.18,1) infinite;
  /* ...but is phase-shifted by a NEGATIVE delay so the four words spread evenly
     across the loop and one is always on screen (4 words * 2s = 8s). */
  animation-delay:calc((var(--i) - 4) * 2s);
}

/* enter (flip up from below) -> hold ~17% -> exit (flip up & away) -> parked */
@keyframes kt-cycle{
  0%    {opacity:0; transform:translateY(112%)  rotateX(-72deg) scale(.92); filter:blur(5px)}
  2.4%  {opacity:1; transform:translateY(0)     rotateX(0)      scale(1.06); filter:blur(0)}
  5.5%  {opacity:1; transform:translateY(0)     rotateX(0)      scale(1)}
  22%   {opacity:1; transform:translateY(0)     rotateX(0)      scale(1)}
  26%   {opacity:0; transform:translateY(-112%) rotateX(72deg)  scale(.92); filter:blur(5px)}
  26.01%,100% {opacity:0; transform:translateY(112%) rotateX(-72deg) scale(.92); filter:blur(5px)}
}

/* centred accent dash that grows from the middle on each swap to punctuate the beat */
.kt-underline{
  position:absolute; left:0; right:0; margin:0 auto; bottom:.04em;
  width:clamp(48px,18%,128px); height:.055em; border-radius:99px;
  background:linear-gradient(90deg,#c8ff3d,#ff5247 38%,#8b7bff 70%,#36e0d6);
  transform:scaleX(0); transform-origin:center; animation:kt-sweep 2s cubic-bezier(.66,0,.3,1) infinite;
}
@keyframes kt-sweep{
  0%{transform:scaleX(0);opacity:0} 12%{opacity:1}
  50%,72%{transform:scaleX(1);opacity:1}
  100%{transform:scaleX(1);opacity:0}
}

/* Reduced motion: freeze to a clean static poster (show ONLY the first word). */
@media (prefers-reduced-motion:reduce){
  .kt-rotor{display:flex; align-items:center; justify-content:center}
  .kt-word{animation:none!important; opacity:0!important; transform:none!important; filter:none!important}
  .kt-word--lead{opacity:1!important; position:relative; inset:auto}
  .kt-underline{animation:none!important; transform:scaleX(1); opacity:.85}
}
```

The drifting background words use the standard duplicated-track marquee (`width:max-content`, two identical spans, `translateX(-50%)` loop) with outline type for texture:

```css
.kt-drifttrack{display:flex; width:max-content; animation:kt-drift 42s linear infinite}
.kt-drifttrack span{font-family:'Anton',sans-serif; font-size:clamp(96px,20vw,280px);
  color:transparent; -webkit-text-stroke:1.4px rgba(244,241,234,.075); text-transform:uppercase}
@keyframes kt-drift{to{transform:translateX(-50%)}}
```

## JavaScript

**Not required for the rotator** — it is 100% CSS. The demo adds two optional flourishes (pointer parallax for depth, and a running SMPTE timecode that sells the "brand-film" framing), both disabled under reduced-motion:

```js
var stage=document.querySelector('.kt-stage');
var reduce=matchMedia('(prefers-reduced-motion: reduce)').matches;
var bg=stage.querySelector('.kt-bg'), hero=stage.querySelector('.kt-hero');

if(!reduce && bg){                      // depth parallax
  stage.addEventListener('pointermove',function(e){
    var r=stage.getBoundingClientRect();
    var x=(e.clientX-r.left)/r.width-.5, y=(e.clientY-r.top)/r.height-.5;
    bg.style.transform   = 'translate3d('+(x*26)+'px,'+(y*18)+'px,0)';
    hero.style.transform = 'translate3d('+(x*-10)+'px,'+(y*-7)+'px,0)';
  });
  stage.addEventListener('pointerleave',function(){ bg.style.transform=hero.style.transform=''; });
}

var tc=document.getElementById('kt-timecode'), base=4*60+21+14/24;  // 00:04:21:14 @ 24fps
if(tc && !reduce){
  var t0=performance.now(), pad=function(n){return(n<10?'0':'')+n;};
  (function loop(now){
    var t=base+(now-t0)/1000, ff=Math.floor((t%1)*24);
    tc.textContent=pad(Math.floor(t/3600)%24)+':'+pad(Math.floor(t/60)%60)+':'+pad(Math.floor(t)%60)+':'+pad(ff);
    requestAnimationFrame(loop);
  })(t0);
}
```

## How it works

- **One animation, N phase-shifts.** Every `.kt-word` runs the *same* `kt-cycle` keyframes; the only difference is `animation-delay: calc((var(--i) - 4) * 2s)`. Because the delay is **negative**, each word starts "already running" at a different point in the loop, so for four words (delays −8s/−6s/−4s/−2s) they appear at 0s/2s/4s/6s and the slot is never empty — even on first paint.
- **The mask clips only top & bottom.** The slot is `height:1.25em`; words flip in from `translateY(112%)` (below) and exit to `translateY(-112%)` (above), so only the held word sits in the visible band. It uses `clip-path:inset(0 -1000px)` rather than `overflow:hidden` so a word wider than the box still shows in full (it spills sideways instead of being chopped).
- **The 3D flip** comes from `rotateX()` on the word plus `perspective` on the parent; a `scale(1.06)` overshoot on entry gives it a tactile "pop", and a small `blur()` on the in/out frames softens the swap.
- **`em`-based sizing** means the slot height, the flip distance and the underline all scale together when you change one `font-size` (here a fluid `clamp()`), so it stays correct from mobile to desktop.

## Customization

- **Words & count:** add/remove `.kt-word` spans, then keep the maths in sync — `animation-duration = N * perWord` and `animation-delay: calc((var(--i) - N) * perWord)`. The lead word (shown when motion is off) should carry `kt-word--lead`.
- **Speed:** change the single `8s` (and the matching `2s` delay step). Longer hold = push the `22%` keyframe later.
- **Motion flavour:** swap `rotateX` for `rotateY` (vertical blinds), or drop the rotate and just `translateY` for a clean slot-machine roll; tune the entry `scale`/`blur` for softer or snappier swaps.
- **Colour:** each word's `--accent` is independent — go monochrome by setting them all equal, or theme the `kt-underline` gradient to match your brand.
- **Type pairing:** the contrast is the point — an industrial condensed face (Anton) against an expressive italic serif (Playfair). Substitute any high-contrast display for the rotor to change the personality.

## Accessibility & performance

- **Respect `prefers-reduced-motion`.** The media query above collapses the loop to a single static word (a legible poster) and stops the marquees, grain blink and timecode. This is essential: a generic `animation-duration:0` reset would otherwise freeze a word *mid-flip*, looking broken.
- **Screen readers get one clean sentence** via the visually-hidden `.kt-sr` span; every animated/duplicated fragment is `aria-hidden="true"` so the cycling letters aren't announced as gibberish.
- **Cheap to run:** only `transform`/`opacity`/`filter` animate (compositor-friendly); `will-change` and `backface-visibility:hidden` are hinted on the words. Keep the rotor word count small and avoid animating layout properties.
- **Contrast:** the bright accents sit on a near-black stage and clear ~7:1; if you lighten the background, darken the accents to hold legibility.

## Gotchas

- **Slot height must match the font.** If the rotor `height` is too small the held word's ascenders/descenders get clipped; too large and the flip "park" position peeks in. `~1.25em` of the rotor's own `font-size` is the sweet spot — keep it in `em`, not `px`.
- **Don't use `overflow:hidden` if a word can exceed the box width.** The box auto-sizes to the surrounding text (here "BRANDS IN MOTION"), so the longest rotating word ("animate") is wider and `overflow:hidden` would chop its sides. `clip-path:inset(0 -1000px)` masks only top/bottom and lets the word overflow sideways — exactly what a centred rotator needs.
- **Negative delay, not positive.** Positive `animation-delay` leaves the later words blank until their first turn; the `var(--i) - N` (negative) form is what fills the slot on load.
- **`perspective` lives on the parent.** Put it on `.kt-rotor`, not on each word, or the `rotateX` looks flat.
- **Fonts load late.** Use `display=swap` (and ideally `preconnect`) or the kinetic word will flash in an unstyled fallback at a different width.
- **Don't put `width:100vw` on the stage** — a contained, rounded `overflow:hidden` panel avoids the horizontal scrollbar that full-bleed breakouts cause.
</content>
</invoke>
