25 CSS Text Animations 25 / 25

CSS Kinetic Typography Animation

Words animate as independent kinetic objects — each with unique scale, rotation, and velocity — echoing the motion graphics discipline of kinetic type design.

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

The code

<div class="ta-25">
  <div class="ta-25__stage" id="ta-25-stage">
    <div class="ta-25__word-wrap" id="ta-25-wrap" aria-label="Design that Moves"></div>
    <p class="ta-25__sub">JS kinetic entry · micro-oscillation · per-word custom properties</p>
  </div>
</div>
.ta-25, .ta-25 *, .ta-25 *::before, .ta-25 *::after {
  margin: 0; padding: 0; box-sizing: border-box;
}
.ta-25 ::selection { background: #4f46e5; color: #fff; }

.ta-25 {
  --bg: #f8fafc;
  min-height: 100vh;
  background: var(--bg);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 2rem;
  font-family: 'Syne', 'Helvetica Neue', sans-serif;
  overflow: hidden;
}

.ta-25__stage { text-align: center; width: 100%; }

.ta-25__word-wrap {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  align-items: center;
  gap: 0.2em 0.35em;
  min-height: 80px;
}

.ta-25__word {
  display: inline-block;
  font-size: clamp(1.8rem, 6vw, 4rem);
  font-weight: 900;
  letter-spacing: -0.02em;
  color: #1e1b4b;
  opacity: 0;
  animation:
    ta-25-enter 0.85s cubic-bezier(0.22, 1, 0.36, 1) forwards,
    ta-25-float var(--float-dur, 4s) ease-in-out var(--float-delay, 0s) infinite alternate;
  animation-delay: var(--delay, 0s), calc(var(--delay, 0s) + 0.85s);
}

.ta-25__word:nth-child(even) { color: #4f46e5; }
.ta-25__word:nth-child(3n)   { color: #7c3aed; }

@keyframes ta-25-enter {
  from {
    opacity: 0;
    transform:
      translate(var(--sx, 0px), var(--sy, -40px))
      rotate(var(--sr, 0deg))
      scale(var(--ss, 0.8));
  }
  to {
    opacity: 1;
    transform: translate(0, 0) rotate(0deg) scale(1);
  }
}

@keyframes ta-25-float {
  from { transform: translateY(0) rotate(0deg); }
  to   { transform: translateY(var(--fy, -6px)) rotate(var(--fr, 1deg)); }
}

.ta-25__sub {
  font-size: 0.65rem;
  color: #94a3b8;
  margin-top: 1rem;
  letter-spacing: 0.1em;
  font-family: 'Courier New', monospace;
}

@media (prefers-reduced-motion: reduce) {
  .ta-25__word { animation: none !important; opacity: 1; transform: none; }
}
(function() {
  const wrap = document.getElementById('ta-25-wrap');
  if (!wrap) return;

  const text  = wrap.getAttribute('aria-label') || 'Design that Moves';
  const words = text.split(' ');

  words.forEach((word, i) => {
    const el = document.createElement('span');
    el.className = 'ta-25__word';
    el.textContent = word;

    const angle = Math.random() * Math.PI * 2;
    const dist  = 80 + Math.random() * 120;
    el.style.setProperty('--sx',         Math.cos(angle) * dist + 'px');
    el.style.setProperty('--sy',         Math.sin(angle) * dist + 'px');
    el.style.setProperty('--sr',         (Math.random() * 40 - 20) + 'deg');
    el.style.setProperty('--ss',         (0.6 + Math.random() * 0.4).toFixed(2));
    el.style.setProperty('--delay',      (i * 0.1 + Math.random() * 0.05) + 's');
    el.style.setProperty('--float-dur',  (3 + Math.random() * 2).toFixed(1) + 's');
    el.style.setProperty('--float-delay', (Math.random() * 1.5).toFixed(2) + 's');
    el.style.setProperty('--fy',         (-(4 + Math.random() * 6)).toFixed(1) + 'px');
    el.style.setProperty('--fr',         ((Math.random() * 2 - 1) * 1.5).toFixed(2) + 'deg');

    wrap.appendChild(el);
  });
})();

How this works

JavaScript assigns each word in the text a set of randomised CSS custom properties defining its entry trajectory: --sx/--sy (starting translate), --sr (starting rotation), --ss (starting scale). These feed into a CSS keyframe that animates all four from their random starting values to their resting translate(0) rotate(0) scale(1) state. The result is each word arrives from a different direction, angle, and distance — like objects converging from chaos.

A secondary animation layer adds a subtle perpetual micro-motion after the entry — a gentle oscillation in translateY and rotate with slow, randomised durations — keeping the settled type alive and energetic. The micro-motion uses animation-direction: alternate and ease-in-out timing to loop smoothly without visible restart snapping.

Customize

  • Increase entry drama by widening translateRange from 150px to 300px and rotateRange from 30deg to 60deg in the JS setup.
  • Disable the micro-motion by removing the second animation in the CSS and leaving only the entry keyframe.
  • Apply colour per word by assigning a --colour custom property from a palette array and referencing it in the CSS as color: var(--colour).
  • Swap the easing from cubic-bezier to spring-style overshoot (cubic-bezier(0.34, 1.56, 0.64, 1)) for words that overshoot and snap back.
  • Connect this to a text input so the user's typed sentence becomes the kinetic display — rebuilding the word spans on each keystroke.

Watch out for

  • Using CSS custom properties for transform values requires them to be set as inline styles before the animation class is added — ensure JS sets the properties synchronously in the same frame as the class toggle.
  • The micro-motion oscillation must use different durations per element or they will sync up into an obvious uniform wave after a few seconds — randomise by ±1s per element.
  • Very large entry transforms combined with overflow: hidden on the container will clip words before they reach their resting position — use overflow: visible and rely on the page container for clipping.

Browser support

ChromeSafariFirefoxEdge
All All All All

CSS custom properties in transform values require Chrome 49+, Safari 9.1+, Firefox 31+. The JS-assigned inline custom properties work universally in modern browsers.

Search CodeFronts

Loading…