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.

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

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>
.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);
})();

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 words array 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 interval value in the setInterval call — 2000ms is snappy, 4000ms is contemplative.
  • Add a colour crossfade alongside the blur by animating color between 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: absolute within a position: relative parent of matching height to prevent layout shifts during the morph.
  • Simultaneous filter: blur() and opacity transitions on overlapping elements can compound GPU layer costs — ensure the container has isolation: 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

ChromeSafariFirefoxEdge
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.

Search CodeFronts

Loading…