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



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, noaria-hiddenor image replacement needed. - No
role,aria-label, oraria-liveis set on the container: since the text is static content (not a notification), this is acceptable; anaria-labelis only useful if the container is presented as an interactive widget. - Letter contrast:
#fffon a#0a0a0fbackground — 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>ortabindex: the element is not keyboard-focusable. Wrap the container in a<button>or addtabindex="0"+keydownhandler for full keyboard accessibility.
Browser compatibility
Requires CSS Transforms (level 1) and CSS Custom Properties — supported in all modern browsers since 2017.
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):
<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>
Full HTML + CSS + JS, copy-paste ready — with hundreds of premium effects.
Customize
Options passed to the API or data-* attributes:
| Option / property | Default | Effect |
|---|---|---|
| --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
.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');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.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.