16 CSS Fade In Animation Designs 16 / 16

Cascade Letter Drop Fade

JavaScript splits a headline character-by-character into spans with staggered animation-delay; each letter drops in from above with a spring bounce, creating a raindrop cascade.

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

The code

<div class="fi-16" id="fi-16-root">
  <div>
    <div class="fi-16__label">Letter cascade</div>
    <span class="fi-16__title" id="fi-16-title">Cascade Letters</span>
    <p class="fi-16__sub" id="fi-16-sub" style="animation-delay:1.4s">JavaScript splits every character into a span with a staggered animation-delay, creating a raindrop-style letter cascade.</p>
    <div class="fi-16__row" id="fi-16-row" style="animation-delay:1.7s">
      <span class="fi-16__chip">JS split</span>
      <span class="fi-16__chip">nth-child delay</span>
      <span class="fi-16__chip">spring bounce</span>
    </div>
    <div class="fi-16__replay" id="fi-16-replay" style="animation-delay:1.9s">↺ replay animation</div>
  </div>
</div>
.fi-16{
  --bg:#090b14;--gold:#f59e0b;--amber:#fbbf24;--text:#fffbeb;
  font-family:'DM Sans',sans-serif;
  min-height:340px;border-radius:20px;
  display:grid;place-items:center;
  padding:40px;overflow:hidden;text-align:center;
}
.fi-16 *,.fi-16 *::before,.fi-16 *::after{box-sizing:border-box;margin:0;padding:0}
.fi-16 ::selection{background:var(--gold);color:#000}

.fi-16__label{
  font-size:.7rem;font-weight:600;letter-spacing:.2em;text-transform:uppercase;
  color:rgba(255,251,235,.35);margin-bottom:20px;
}
.fi-16__title{
  font-family:'Syne',sans-serif;font-size:clamp(1.8rem,4vw,3rem);font-weight:800;
  color:var(--text);line-height:1.2;margin-bottom:16px;display:block;
}
/* JS injects letter spans */
.fi-16__letter{
  display:inline-block;
  opacity:0;transform:translateY(-20px) rotate(-8deg);
  animation:fi-16-letter-drop .5s cubic-bezier(.34,1.56,.64,1) forwards;
}
.fi-16__letter.fi-16--space{margin-right:.35em}

.fi-16__sub{
  font-size:.95rem;color:rgba(255,251,235,.45);line-height:1.6;max-width:400px;
  margin-bottom:28px;opacity:0;animation:fi-16-fade-up .6s ease forwards;
}
.fi-16__row{display:flex;gap:10px;justify-content:center;flex-wrap:wrap;opacity:0;animation:fi-16-fade-up .5s ease forwards}

.fi-16__chip{
  padding:7px 16px;border-radius:20px;font-size:.75rem;font-weight:700;
  background:rgba(245,158,11,.12);border:1px solid rgba(245,158,11,.25);color:var(--gold);
  transition:background .2s,transform .2s;cursor:default;
}
.fi-16__chip:hover{background:rgba(245,158,11,.22);transform:translateY(-2px)}
.fi-16__replay{
  margin-top:18px;display:inline-block;font-size:.72rem;font-weight:600;
  color:rgba(255,251,235,.35);cursor:pointer;border-bottom:1px dashed rgba(255,255,255,.15);
  padding-bottom:2px;transition:color .2s;
  opacity:0;animation:fi-16-fade-up .5s ease forwards;
}
.fi-16__replay:hover{color:var(--gold)}

@keyframes fi-16-letter-drop{to{opacity:1;transform:translateY(0) rotate(0deg)}}
@keyframes fi-16-fade-up{to{opacity:1}}

@media(prefers-reduced-motion:reduce){
  .fi-16 *{animation:none!important;opacity:1!important;transform:none!important}
}
(function(){
  const titleEl = document.getElementById('fi-16-title');
  const replay  = document.getElementById('fi-16-replay');
  const STEP = 0.05;

  function animate(){
    const text = 'Cascade Letters';
    titleEl.innerHTML = '';
    [...text].forEach((ch, i)=>{
      const span = document.createElement('span');
      span.className = 'fi-16__letter' + (ch === ' ' ? ' fi-16--space' : '');
      span.textContent = ch === ' ' ? '\u00a0' : ch;
      span.style.animationDelay = (0.1 + i * STEP)+'s';
      titleEl.appendChild(span);
    });
  }

  animate();

  replay.addEventListener('click',()=>{
    titleEl.querySelectorAll('.fi-16__letter').forEach(s=>{
      s.style.animation='none';void s.offsetWidth;
      s.style.animation='fi-16-letter-drop .5s cubic-bezier(.34,1.56,.64,1) forwards';
    });
  });
})();

How this works

The JavaScript function iterates over each character in the headline string using [...text].forEach (spread to correctly handle multi-byte chars). Each character becomes a span.fi-16__letter with animation-delay: 0.1 + index × 0.05 + 's'. The keyframe starts each letter at opacity: 0; transform: translateY(-20px) rotate(-8deg) — tipped and airborne — and settles to upright via cubic-bezier(.34, 1.56, .64, 1) spring overshoot.

Spaces receive a .fi-16--space class with a margin-right: .35em so word gaps are preserved. The replay handler iterates all letter spans, nullifies their animation with 'none', forces a reflow via offsetWidth access to flush the browser's style cache, then restores the animation string — this is the canonical pure-JS animation restart technique without cloning nodes.

Customize

  • Reduce letter delay STEP from 0.05 to 0.03s for faster cascade — good for longer headlines.
  • Change the keyframe to translateY(20px → 0) (upward from below) for letters rising rather than dropping.
  • Reduce initial rotate(-8deg) to -3deg for a subtler tilt on each letter.
  • Apply different colors to vowels vs consonants via post-split CSS class assignment in the JS function.

Watch out for

  • Spreading a string with [...text] correctly handles emoji and multi-byte Unicode — text.split('') would split emoji into surrogate pairs, corrupting the characters.
  • If the headline text changes dynamically, call the animate() function again — the old spans are replaced automatically since innerHTML is cleared first.
  • The animation: none → reflow → restore technique for replay fires a synchronous layout via offsetWidth. In tight loops, batch reflows by reading all offsetWidth values first, then writing all animation resets.

Browser support

ChromeSafariFirefoxEdge
60+ 12+ 60+ 60+

Spread operator [...text] for Unicode-safe split requires ES6; works in all evergreen browsers

Search CodeFronts

Loading…