Léa
your granddaughter is calling
On the call · 00:47
A 50 px ball that jumps to 60 px, lands squashing (1.15 × 0.85), bounces to 25 then 8 px and settles, in 2 s: a single CSS keyframe whose easing overshoots its target at each step, hence the elasticity — zero JavaScript, a simple pulse when the user reduces motion.
Updated
This effect is free — the Transitions category contains 52 effects total, including 9 free. Effect.Labs has 840 vanilla effects. Explore the category →
your granddaughter is calling
On the call · 00:47
Your turn, Jules!Tap the die to roll it
4! Move 4 squaresThe goose sends you on again
3 minutes: shoulders, back, a glass of water
Roll your shoulders back, 10 times
Three elements: the .md-spring-stage frame (250 px tall, background #1a1a24), the .md-spring area that centers, and .md-spring-ball, a 50 px disc in an indigo to pink gradient (#6366f1 → #ec4899). All the physics lives in @keyframes mdSpring, played in a loop: animation: mdSpring 2s cubic-bezier(0.34, 1.56, 0.64, 1) infinite. The steps alternate jumps and contacts: 20% in the air at −60 px, stretched (0.9 × 1.1); 40% on the ground, squashed (1.15 × 0.85); 55% at −25 px; 70% on the ground (1.05 × 0.95); 85% at −8 px; 100% at rest. Each bounce is roughly 40% of the previous one, and the deformations shrink with it: that is what makes it feel like a material absorbing the impact.
The easing does the rest. Its second value, 1.56, is above 1: between two steps, the ball goes a little past the target before coming back. Measured frame by frame in Chromium 151: top of the first jump at −65.8 px instead of −60, a 5.8 px dip below the resting point at contact, maximum squash 1.17 × 0.83. That overshoot, applied to every segment, acts as the spring; with an ease-in-out curve, the same keyframe would give a ball going up and down without bouncing. The transform origin stays at the center: when squashed, the ball shrinks toward its middle and its bottom rises by about 4 px — see the settings to rest it on a floor. Only transform is animated, with no layout recalculation.
The effect is an attention call that loops, not a transition between two states: it moves as long as something is waiting for an action. It is up to your page to stop it when the action happens (call answered, die rolled): animation: none on the ball puts it back at rest, as in the three examples. The frame has its own class, prefixed like the rest of the effect: two effects from the library pasted on the same page do not overwrite each other.
prefers-reduced-motion: reduce, the bounce keyframe is replaced by mdSpringFade, an opacity pulse from 1 to 0.45 on the same 2 s rhythm. Measured in Chromium 151, Firefox 153 and WebKit 26: no more movement or deformation (identical transform at every measurement), changing opacity. The "something is waiting" signal stays readable without motion.animation: none as soon as it happens, and do not leave the ball bouncing in a part of the page that is not waiting for anything.div with no role. If it signals a call or a game turn, also write it as text next to it ("Léa is calling"); if it acts as a button, give it role="button", tabindex="0" and a label, or put it inside a real button.#1a1a24 background; on a light background, check the indigo end, the darker one.Pure CSS (@keyframes, transform, cubic-bezier with a value outside 0-1), 1.4 KB, zero JavaScript. Measured in Chromium 151, Firefox 153 and WebKit 26: same bounce, same replacement pulse under reduced motion. A cubic-bezier curve whose y value exceeds 1 is valid in the spec and read by all three engines.
Without CSS animations, the ball stays still in the center of its frame, round and in color: the element shows, only the bounce is missing.
Copy the three blocks into your page. No dependencies.
<div class="md-spring-stage">
<div class="md-spring">
<div class="md-spring-ball"></div>
</div>
</div>
.md-spring-stage,
.md-spring-stage *,
.md-spring-stage *::before,
.md-spring-stage *::after {
box-sizing: border-box
}
.md-spring-stage {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 250px;
overflow: hidden;
position: relative;
background: rgb(26, 26, 36);
color: rgb(255, 255, 255);
font-family: sans-serif;
}
.md-spring {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.md-spring-ball {
width: 50px;
height: 50px;
border-radius: 50%;
background: linear-gradient(135deg, #6366f1, #ec4899);
animation: mdSpring 2s cubic-bezier(0.34, 1.56, 0.64, 1) infinite;
}
@keyframes mdSpring {
0%,
100% {
transform: translateY(0) scaleX(1) scaleY(1);
}
20% {
transform: translateY(-60px) scaleX(0.9) scaleY(1.1);
}
40% {
transform: translateY(0) scaleX(1.15) scaleY(0.85);
}
55% {
transform: translateY(-25px) scaleX(0.95) scaleY(1.05);
}
70% {
transform: translateY(0) scaleX(1.05) scaleY(0.95);
}
85% {
transform: translateY(-8px);
}
}
/* Mouvement réduit : ni saut ni déformation, une pulsation d'opacité garde le signal */
@media (prefers-reduced-motion: reduce) {
.md-spring-ball {
animation: mdSpringFade 2s ease-in-out infinite;
}
}
@keyframes mdSpringFade {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.45;
}
}
Options passed to the API or data-* attributes:
| Option / property | Default | Effect |
|---|---|---|
Jump heights (CSS — translateY at 20%, 55% and 85%) |
−60 / −25 / −8 px | Keep a ratio close to 0.4 between two bounces for believable damping: −90 / −36 / −14 px for a bigger ball. Equal jumps give a ball that never loses energy. |
Squash and stretch (CSS — scaleX / scaleY) |
1.15 × 0.85 at first contact, 0.9 × 1.1 in the air | 1.3 × 0.7 gives a very soft material (jelly); 1.05 × 0.95 a tennis ball. Keep the product of the two close to 1 so the ball seems to keep its volume. |
Easing (CSS — cubic-bezier(0.34, 1.56, 0.64, 1)) |
about 10% overshoot | 1.56 → 1.9 strengthens the spring (higher top, deeper dip); 1.56 → 1 removes the overshoot and the elastic feel. |
Duration (CSS — 2s) |
2 s per full cycle | 1.4 s makes the ball jumpy; 3 s heavier. For a bounce followed by a real pause, stretch the keyframe: end the steps at 60% and leave the ball at rest until 100%. |
Stop (CSS — animation: none) |
none: infinite loop | Set by a state class (e.g. .is-answered .md-spring-ball), it puts the ball back at rest immediately. To finish the current jump, wait for the animationiteration event before adding the class. |
Size and color (CSS — .md-spring-ball) |
50 px, #6366f1 → #ec4899 gradient |
Any element can bounce: a photo, an icon, a die. Distances are in pixels, so a 100 px ball keeps 60 px jumps; raise them with the size. |
Add a state class on a parent and the rule .is-done .md-spring-ball { animation: none; }: the ball returns to rest at once. If the jump gets cut mid-air and it shows, listen for animationiteration on the ball and add the class at that moment, when it touches the ground at the end of a cycle. To restart, remove the class: the animation begins again from the start.
Because of the cubic-bezier(0.34, 1.56, 0.64, 1) curve: its second value is above 1, so each segment goes past its target before coming back. Measured: 5.8 px below rest at contact, top at 65.8 px instead of 60. That overshoot is what makes it springy. If the ball sits on a floor line and the dip shows, raise the floor by 6 px or lower 1.56 to 1.3.
Add transform-origin: 50% 100% to .md-spring-ball. By default, squashing happens around the center: at contact, the bottom of the ball rises by about 4 px and it seems to float. With the origin at the bottom, the bottom stays on the floor and only the height flattens, like a real ball. The jumps (translateY) do not change.