Text✨ Premium

Elastic Bounce Letters

Each letter bounces from a squished position via a damped spring CSS animation — per-letter configurable stagger, click-to-replay, zero dependencies.

CSSSpringBounce

You'll get access to the interactive demo with a free account.

This effect is part of Effect.Labs — 811 vanilla effects, some free, some premium. Text has 56 effects, including 4 free. Explore the category →

3 usage examples

Agency hero — Full-width hook verb — Elastic Bounce Letters example 1

① Agency hero — Full-width hook verb

WhenOn entry to an agency site, portfolio, or startup page where a single strong keyword is centered on a dark background.
WhyThe letter-by-letter bounce turns a plain word into a visual event: the eye follows each character and retains the message. The damped spring physics convey dynamism without the overused typewriter effect.
Settingsfont-size: 5rem; font-weight: 900; on .kt-text, --delay of 0.08 s between letters to slow the entry and give it more weight.
Score reveal — Quiz result letter by letter — Elastic Bounce Letters example 2

② Score reveal — Quiz result letter by letter

WhenAfter a quiz or challenge is submitted, when the score or result word (PERFECT, 87/100) replaces the Submit button — turning the result into a visual event rather than a plain number.
WhyThe damped spring turns each letter into a physical punch: the squish-and-bounce amplifies satisfaction on a good score (or surprise on a bad one). The trigger is the submit click, not page load, anchoring the reward to the user's action.
Settingsfont-size: 3.4rem; font-weight: 900; color: #34d399; on .kt-text (green for success, red #f87171 for failure), --delay of 0.07 s so each letter strikes distinctly, triggered by .classList.add('active') in the Submit button handler.
Pre-launch page — AVAILABLE revealed on click — Elastic Bounce Letters example 3

③ Pre-launch page — AVAILABLE revealed on click

WhenOn a countdown or waitlist page, when clicking 'Join the list' triggers the AVAILABLE animation letter by letter, turning the form submission into a visible transition moment.
WhyThis context exploits the click trigger — distinct from a simple page load — so the elastic bounce physically embodies the state transition (waiting → registered). The animation lasts 0.8 s and plays once: it marks the moment without dragging it out.
Settingsfont-size: 2.8rem; font-weight: 900; color: #818cf8; on .kt-text (purple to reinforce brand), --delay of 0.06 s (default stagger — 10 letters stay fast at 0.6 s total), animation-duration: .8s kept, cubic-bezier(.34, 1.56, .64, 1) for maximum overshoot.

How it works

The core is @keyframes ktElastic: at 0% the letter is flattened (scaleY(0)) and stretched wide (scaleX(1.8)) — a compressed spring. At 40% it overshoots upward (scaleY(1.3)) while pinching inward (scaleX(.7)). Two additional keyframes at 60% and 80% simulate the damped oscillations of a real spring before settling at 1×1.

The easing curve cubic-bezier(.34, 1.56, .64, 1) amplifies the overshoot: the third control point at 1.56 pushes past the target value, replicating spring physics without JavaScript. Total duration is 0.8 s with fill-mode: forwards to hold the final state.

Each letter is a <span class="char"> with display:inline-block (required for 2D transforms on inline nodes). The CSS custom property --delay is set inline on each span (0 s, 0.06 s, 0.12 s…) and referenced via animation-delay: var(--delay): a single animation pass, staggered letter by letter.

Adding the class .active to the .kt-elastic container triggers the cascade. To replay: remove .active, force a reflow (el.offsetWidth), then add it back. The JS utility splitChars(el) dynamically splits a node's text into <span class="char"> elements with correctly spaced --delay values.

Accessibility

  • prefers-reduced-motion absent: no @media (prefers-reduced-motion) rule exists in the provided code. Motion-sensitive users will see the full animation. Add @media (prefers-reduced-motion:reduce) { .kt-elastic .char { animation: none; opacity:1; transform:none; } } to protect them.
  • Text content is real text inside <span> elements — natively readable by all screen readers, no aria-hidden or image replacement needed.
  • No role, aria-label, or aria-live is set on the container: since the text is static content (not a notification), this is acceptable; an aria-label is only useful if the container is presented as an interactive widget.
  • Letter contrast: #fff on a #0a0a0f background — ratio ≈ 18.1:1, far above WCAG AA (4.5:1) and AAA (7:1) thresholds.
  • Click-to-replay relies on style="cursor:pointer" with no <button> or tabindex: the element is not keyboard-focusable. Wrap the container in a <button> or add tabindex="0" + keydown handler for full keyboard accessibility.

Browser compatibility

Requires CSS Transforms (level 1) and CSS Custom Properties — supported in all modern browsers since 2017.

Chrome 90+✓ Full
Firefox 89+✓ Full
Safari 15+✓ Full
Edge 90+✓ Full
Mobile iOS✓ Full
Android Chrome✓ Full

Without CSS Animations, letters stay invisible (<code>opacity:0</code>) because the initial state is set via inline style. Add <code>.no-js .kt-elastic .char { opacity:1; transform:none; }</code> or strip the inline styles server-side for legacy browsers.

The code

HTML structure to paste into your page (CSS + JS available with a premium account):

index.html — structure
<div class="kt-text kt-elastic" id="elastic1" data-text="TITLE" style="cursor:pointer;">
  <!-- splitChars() generates one <span class="char"> per character with a progressive --delay -->
  <!-- Example for "TITLE" (5 letters, 0.06 s stagger): -->
  <span class="char" style="--delay:0s;">T</span>
  <span class="char" style="--delay:0.06s;">I</span>
  <span class="char" style="--delay:0.12s;">T</span>
  <span class="char" style="--delay:0.18s;">L</span>
  <span class="char" style="--delay:0.24s;">E</span>
  <!-- Add .active class to the div to launch the animation -->
</div>
🔒 Unlock the full code — from €2.99 the first month

Full HTML + CSS + JS, copy-paste ready — with hundreds of premium effects.

Customize

Options passed to the API or data-* attributes:

Option / propertyDefaultEffect
--delay (inline on each .char) 0 s, 0.06 s, 0.12 s… Per-letter stagger delay. Increase to 0.10 s for a slower, more dramatic entry; decrease to 0.03 s for a near-simultaneous bounce.
animation-duration (.kt-elastic.active .char) .8s Bounce duration per letter. 0.5 s = snappy, 1.2 s = cinematic and fluid.
animation-timing-function (.kt-elastic.active .char) cubic-bezier(.34, 1.56, .64, 1) Spring curve. Raise the 3rd coefficient (1.56) for a more pronounced overshoot; lower it toward 1.0 to remove the overshoot entirely.
font-size (.kt-text) 2.2rem Display size. The effect is most impressive at large sizes (4–6 rem) where the squish and bounce are clearly visible.
font-weight (.kt-text) 800 Weight. 900 amplifies the visual impact of the bounce; 400 gives a lighter, editorial feel.
color (.kt-text) #fff Letter color. Change freely — contrast must remain ≥ 4.5:1 against the background in use.
splitChars(el) Globally exposed JS utility: dynamically splits a .kt-elastic node's text into <span class="char"> elements with progressive --delay values. Call it before adding .active if the text is server-rendered without pre-splitting.

FAQ

Remove the .active class from the container, force a reflow with el.offsetWidth (reading any layout property is enough), then add .active back. Example: el.classList.remove('active'); void el.offsetWidth; el.classList.add('active');
Yes. The splitChars() function preserves spaces as <span> elements containing a non-breaking space (\u00a0). Each space receives its own --delay, producing a natural pause in the bounce between words.
With pure CSS (Scroll-driven Animations), yes — bind animation-timeline: view() and animation-range on the .char elements to replace the class toggle. For broader compatibility (current Firefox and Safari), prefer an IntersectionObserver that adds .active when the container reaches 20% of the viewport.