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.
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> <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; }
} .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);
});
})(); (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
translateRangefrom150pxto300pxandrotateRangefrom30degto60degin 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
--colourcustom property from a palette array and referencing it in the CSS ascolor: 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: hiddenon the container will clip words before they reach their resting position — useoverflow: visibleand rely on the page container for clipping.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 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.