25 CSS Text Animations 22 / 25

CSS Elastic Bounce Text Animation

A headline drops in with exaggerated squash-and-stretch physics — JavaScript assigns custom spring parameters per letter for natural variance.

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

The code

<div class="ta-22">
  <div class="ta-22__stage">
    <h2 class="ta-22__text" id="ta-22-text" aria-label="BOUNCE">BOUNCE</h2>
    <button class="ta-22__btn" id="ta-22-replay">↺ Drop again</button>
  </div>
</div>
.ta-22, .ta-22 *, .ta-22 *::before, .ta-22 *::after {
  margin: 0; padding: 0; box-sizing: border-box;
}
.ta-22 ::selection { background: #b45309; color: #fff; }

.ta-22 {
  --bg: linear-gradient(180deg, #0f0a00, #1a0f00);
  min-height: 100vh;
  background: var(--bg);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 1.5rem;
  padding: 2rem;
  font-family: 'Syne', 'Helvetica Neue', sans-serif;
  overflow: hidden;
}

.ta-22__text {
  font-size: clamp(3rem, 10vw, 6rem);
  font-weight: 900;
  letter-spacing: 0.05em;
  display: flex;
  justify-content: center;
}

.ta-22__char {
  display: inline-block;
  color: #fbbf24;
  text-shadow: 0 4px 16px rgba(251,191,36,0.35);
  opacity: 0;
  animation: ta-22-drop 0.9s cubic-bezier(0.215, 0.61, 0.355, 1) forwards;
  animation-delay: var(--delay, 0s);
  transform-origin: center bottom;
}

@keyframes ta-22-drop {
  0%   { opacity: 1; transform: translateY(var(--fall, -80px)) scaleX(1) scaleY(1); }
  55%  { transform: translateY(0) scaleX(1.25) scaleY(0.72); }
  70%  { transform: translateY(-18px) scaleX(0.92) scaleY(1.1); }
  83%  { transform: translateY(0) scaleX(1.1) scaleY(0.9); }
  92%  { transform: translateY(-6px) scaleX(0.97) scaleY(1.04); }
  100% { opacity: 1; transform: translateY(0) scaleX(1) scaleY(1); }
}

.ta-22__btn {
  font-family: inherit;
  font-size: 0.72rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  background: none;
  border: 1px solid #451a03;
  color: #92400e;
  padding: 0.4rem 1rem;
  border-radius: 4px;
  cursor: pointer;
  transition: border-color 0.2s, color 0.2s;
}
.ta-22__btn:hover { border-color: #fbbf24; color: #fbbf24; }

@media (prefers-reduced-motion: reduce) {
  .ta-22__char { animation: none; opacity: 1; transform: none; }
}
(function() {
  const wrap = document.getElementById('ta-22-text');
  const btn  = document.getElementById('ta-22-replay');
  if (!wrap) return;

  const word = wrap.getAttribute('aria-label') || 'BOUNCE';

  function build() {
    wrap.innerHTML = '';
    [...word].forEach((ch, i) => {
      const span = document.createElement('span');
      span.className = 'ta-22__char';
      span.textContent = ch;
      const fall  = -(60 + Math.random() * 80);
      const delay = i * 0.07 + Math.random() * 0.04;
      span.style.setProperty('--fall',  fall + 'px');
      span.style.setProperty('--delay', delay + 's');
      wrap.appendChild(span);
    });
  }

  function replay() {
    wrap.querySelectorAll('.ta-22__char').forEach(s => {
      s.style.animation = 'none';
      void s.offsetWidth;
      s.style.animation = '';
    });
    build();
  }

  build();
  if (btn) btn.addEventListener('click', replay);
})();

How this works

JavaScript splits the headline into letter spans and assigns each one randomised CSS custom properties: --fall-height (how far above the frame each letter starts), --squash (how much it flattens on contact), and --delay (its stagger offset). The CSS ta-22-drop keyframe reads these values to produce a multi-step drop: fall from above, contact squash (scaleX(1.3) scaleY(0.7)), first bounce, smaller bounce, settle — mimicking a real rubber object's diminishing rebound energy.

The squash and stretch phases happen at precise keyframe percentages: 60% is the squash at contact, 75% is the first stretch upward, 85% is the second touch, 100% is settled. Each letter's unique --delay and --fall-height means no two letters have exactly the same entry, giving the illusion of independently dropped objects rather than a rigidly synchronised animation.

Customize

  • Increase the fall height range in the JS by widening fallRange from 80 to 160 for more dramatic drops from higher above the frame.
  • Reduce the squash intensity by changing the keyframe's scaleX(1.3) scaleY(0.7) values closer to 1 for subtler deformation.
  • Add a shadow that compresses during squash: use a box-shadow or text-shadow that expands horizontally during the squash keyframe to suggest a contact shadow.
  • Change easing from the multi-step keyframe to a single cubic-bezier(0.34, 1.56, 0.64, 1) for a simpler elastic feel that's easier to tune.
  • Trigger on scroll-entry using an IntersectionObserver that adds the animation class when the element becomes visible.

Watch out for

  • The squash transform uses both scaleX and scaleY — these must be on an inline-block span, not an inline element, or the scale has no effect.
  • Letters at different fall heights land at slightly different times even with the same delay, creating a subtle misalignment if --fall-height variance is too large — cap at ±60px for controlled results.
  • Replay logic must reset the animation by briefly removing and re-adding the class, using void el.offsetWidth as a forced reflow to flush the browser's animation cache.

Browser support

ChromeSafariFirefoxEdge
All All All All

CSS custom properties in animations are supported in all modern browsers. The JS split adds inline styles before animation triggers — works universally.

Search CodeFronts

Loading…