25 CSS Text Animations 16 / 25
CSS Morphing Text Animation
Two text strings dissolve into each other with a blurred crossfade using CSS filter and opacity — JavaScript cycles through a word list on a timer.
The code
<div class="ta-16">
<div class="ta-16__stage">
<p class="ta-16__label">We build</p>
<div class="ta-16__morph-wrap" id="ta-16-wrap">
<span class="ta-16__word ta-16__word--a" id="ta-16-a">Experiences</span>
<span class="ta-16__word ta-16__word--b" id="ta-16-b">Products</span>
</div>
<p class="ta-16__sub">filter:blur crossfade · JS word cycling</p>
</div>
</div> <div class="ta-16">
<div class="ta-16__stage">
<p class="ta-16__label">We build</p>
<div class="ta-16__morph-wrap" id="ta-16-wrap">
<span class="ta-16__word ta-16__word--a" id="ta-16-a">Experiences</span>
<span class="ta-16__word ta-16__word--b" id="ta-16-b">Products</span>
</div>
<p class="ta-16__sub">filter:blur crossfade · JS word cycling</p>
</div>
</div>.ta-16, .ta-16 *, .ta-16 *::before, .ta-16 *::after {
margin: 0; padding: 0; box-sizing: border-box;
}
.ta-16 ::selection { background: #db2777; color: #fff; }
.ta-16 {
--bg: #0a0015;
--pink: #f472b6;
--purple: #a855f7;
min-height: 100vh;
background: var(--bg);
display: flex;
align-items: center;
justify-content: center;
padding: 2rem;
font-family: 'Syne', 'Helvetica Neue', sans-serif;
}
.ta-16__stage { text-align: center; }
.ta-16__label {
font-size: 0.78rem;
color: #6b21a8;
letter-spacing: 0.2em;
text-transform: uppercase;
margin-bottom: 0.3rem;
}
.ta-16__morph-wrap {
position: relative;
height: clamp(3.5rem, 9vw, 6rem);
display: flex;
align-items: center;
justify-content: center;
}
.ta-16__word {
position: absolute;
font-size: clamp(2.2rem, 7vw, 4.5rem);
font-weight: 900;
white-space: nowrap;
transition: opacity 0.6s ease, filter 0.6s ease;
}
.ta-16__word--a {
background: linear-gradient(90deg, var(--pink), var(--purple));
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
.ta-16__word--b {
background: linear-gradient(90deg, var(--purple), #60a5fa);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
opacity: 0;
filter: blur(12px);
}
.ta-16__word.ta-16--out {
opacity: 0;
filter: blur(16px);
}
.ta-16__word.ta-16--in {
opacity: 1;
filter: blur(0);
}
.ta-16__sub {
font-size: 0.65rem;
color: #2d0a40;
margin-top: 0.8rem;
letter-spacing: 0.1em;
font-family: 'Courier New', monospace;
}
@media (prefers-reduced-motion: reduce) {
.ta-16__word { transition: none; filter: none; }
} .ta-16, .ta-16 *, .ta-16 *::before, .ta-16 *::after {
margin: 0; padding: 0; box-sizing: border-box;
}
.ta-16 ::selection { background: #db2777; color: #fff; }
.ta-16 {
--bg: #0a0015;
--pink: #f472b6;
--purple: #a855f7;
min-height: 100vh;
background: var(--bg);
display: flex;
align-items: center;
justify-content: center;
padding: 2rem;
font-family: 'Syne', 'Helvetica Neue', sans-serif;
}
.ta-16__stage { text-align: center; }
.ta-16__label {
font-size: 0.78rem;
color: #6b21a8;
letter-spacing: 0.2em;
text-transform: uppercase;
margin-bottom: 0.3rem;
}
.ta-16__morph-wrap {
position: relative;
height: clamp(3.5rem, 9vw, 6rem);
display: flex;
align-items: center;
justify-content: center;
}
.ta-16__word {
position: absolute;
font-size: clamp(2.2rem, 7vw, 4.5rem);
font-weight: 900;
white-space: nowrap;
transition: opacity 0.6s ease, filter 0.6s ease;
}
.ta-16__word--a {
background: linear-gradient(90deg, var(--pink), var(--purple));
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
.ta-16__word--b {
background: linear-gradient(90deg, var(--purple), #60a5fa);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
opacity: 0;
filter: blur(12px);
}
.ta-16__word.ta-16--out {
opacity: 0;
filter: blur(16px);
}
.ta-16__word.ta-16--in {
opacity: 1;
filter: blur(0);
}
.ta-16__sub {
font-size: 0.65rem;
color: #2d0a40;
margin-top: 0.8rem;
letter-spacing: 0.1em;
font-family: 'Courier New', monospace;
}
@media (prefers-reduced-motion: reduce) {
.ta-16__word { transition: none; filter: none; }
}(function() {
const words = ['Experiences', 'Products', 'Futures', 'Stories', 'Journeys'];
const elA = document.getElementById('ta-16-a');
const elB = document.getElementById('ta-16-b');
if (!elA || !elB) return;
let idx = 0;
let showingA = true;
function morph() {
idx = (idx + 1) % words.length;
const incoming = showingA ? elB : elA;
const outgoing = showingA ? elA : elB;
incoming.textContent = words[idx];
outgoing.classList.add('ta-16--out');
outgoing.classList.remove('ta-16--in');
incoming.classList.add('ta-16--in');
incoming.classList.remove('ta-16--out');
incoming.style.opacity = '1';
incoming.style.filter = 'blur(0)';
outgoing.style.opacity = '0';
outgoing.style.filter = 'blur(16px)';
showingA = !showingA;
}
setInterval(morph, 2400);
})(); (function() {
const words = ['Experiences', 'Products', 'Futures', 'Stories', 'Journeys'];
const elA = document.getElementById('ta-16-a');
const elB = document.getElementById('ta-16-b');
if (!elA || !elB) return;
let idx = 0;
let showingA = true;
function morph() {
idx = (idx + 1) % words.length;
const incoming = showingA ? elB : elA;
const outgoing = showingA ? elA : elB;
incoming.textContent = words[idx];
outgoing.classList.add('ta-16--out');
outgoing.classList.remove('ta-16--in');
incoming.classList.add('ta-16--in');
incoming.classList.remove('ta-16--out');
incoming.style.opacity = '1';
incoming.style.filter = 'blur(0)';
outgoing.style.opacity = '0';
outgoing.style.filter = 'blur(16px)';
showingA = !showingA;
}
setInterval(morph, 2400);
})();How this works
Two absolutely-positioned text elements occupy the same space. JavaScript alternates which is the 'current' and which is the 'next' word by toggling CSS classes. The morph transition uses filter: blur() simultaneously on both elements — the outgoing word blurs out and fades while the incoming word blurs in then sharpens. A CSS custom property --morph drives the blur amount, interpolated via a CSS transition on both elements.
The dissolve effect is achieved because both elements share the same position and animate in opposite phase: as one's opacity and blur go to zero, the other's come from zero. The midpoint where both are at 50% opacity with medium blur creates the transition zone where the words appear to briefly merge into a single cloud of text before the new word resolves out of it.
Customize
- Change the word list by editing the
wordsarray in the script — any number of words works as long as the transition duration matches the per-word display time. - Adjust blur intensity by changing
--blur-max— higher values (e.g.30px) create a more dramatic fog; lower (8px) creates a subtler haze. - Change display time between morphs by editing the
intervalvalue in thesetIntervalcall —2000ms is snappy,4000ms is contemplative. - Add a colour crossfade alongside the blur by animating
colorbetween each word's assigned hue using CSS custom properties updated by JavaScript. - Replace words with multi-word phrases by widening the container and using
white-space: nowrap— the blur morph works equally well on full sentences.
Watch out for
- Both text elements must be
position: absolutewithin aposition: relativeparent of matching height to prevent layout shifts during the morph. - Simultaneous
filter: blur()andopacitytransitions on overlapping elements can compound GPU layer costs — ensure the container hasisolation: isolate. - The crossfade timing must be carefully matched: too fast produces a snap rather than a dissolve, too slow creates a period where both words are equally legible and compete for attention.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 53+ | 9.1+ | 35+ | 53+ |
CSS filter transitions are supported in all modern browsers. The JS word-cycle degrades gracefully — the first word stays visible if JS fails.