Physics-based motion with natural overshoot.
bouncy spring · overshoots then settles, unlike a plain ease
<div class="mot-spr-stage" data-bounce="bouncy">
<div class="mot-spr-track">
<span class="mot-spr-dot mot-spr-dot--loop" style="background:#A8E6C1;"></span>
</div>
<p class="mot-spr-caption">bouncy spring · overshoots then settles, unlike a plain ease</p>
</div>
<style>
.mot-spr-stage { width: 100%; max-width: 320px; padding: 16px; background: #FAF7F2; border: 1px solid #24143022; border-radius: 12px; font: 12px system-ui, sans-serif; color: #241430aa; }
.mot-spr-track { position: relative; height: 26px; }
.mot-spr-dot { position: absolute; top: 0; left: 1px; width: 22px; height: 22px; border-radius: 999px; }
.mot-spr-caption { margin: 10px 0 0; text-transform: capitalize; }
@media (prefers-reduced-motion: no-preference) {
.mot-spr-dot { animation-duration: 900ms; animation-fill-mode: both; }
[data-bounce="gentle"] .mot-spr-dot { animation-name: mot-spr-gentle; animation-timing-function: cubic-bezier(.25,1.1,.4,1); }
[data-bounce="bouncy"] .mot-spr-dot { animation-name: mot-spr-bouncy; animation-timing-function: cubic-bezier(.34,1.56,.64,1); }
[data-bounce="wobbly"] .mot-spr-dot { animation-name: mot-spr-wobbly; animation-timing-function: cubic-bezier(.68,-0.6,.32,1.6); }
.mot-spr-dot--loop { animation-iteration-count: infinite; }
@keyframes mot-spr-gentle { from { left: 1px; } to { left: 260px; } }
@keyframes mot-spr-bouncy { from { left: 1px; } to { left: 260px; } }
@keyframes mot-spr-wobbly { from { left: 1px; } to { left: 260px; } }
}
</style><div class="w-full max-w-[320px] p-4 bg-[#FAF7F2] border border-[#24143022] rounded-xl font-sans text-xs text-[#241430]/70" data-bounce="bouncy">
<div class="relative h-[26px]">
<span class="absolute top-0 left-px w-[22px] h-[22px] rounded-full tw-spr-loop" style="background:#A8E6C1; animation-duration:900ms; animation-fill-mode:both;"></span>
</div>
<p class="mt-2.5 capitalize">bouncy spring · overshoots then settles, unlike a plain ease</p>
</div>
<style>
@media (prefers-reduced-motion: no-preference) {
[data-bounce="gentle"] span { animation-name: mot-spr-tw-gentle; animation-timing-function: cubic-bezier(.25,1.1,.4,1); }
[data-bounce="bouncy"] span { animation-name: mot-spr-tw-bouncy; animation-timing-function: cubic-bezier(.34,1.56,.64,1); }
[data-bounce="wobbly"] span { animation-name: mot-spr-tw-wobbly; animation-timing-function: cubic-bezier(.68,-0.6,.32,1.6); }
.tw-spr-loop { animation-iteration-count: infinite; }
@keyframes mot-spr-tw-gentle { from { left: 1px; } to { left: 260px; } }
@keyframes mot-spr-tw-bouncy { from { left: 1px; } to { left: 260px; } }
@keyframes mot-spr-tw-wobbly { from { left: 1px; } to { left: 260px; } }
}
</style>export function SpringAnimation({
bounce = "bouncy",
duration = 900,
loop = true,
accent = "#A8E6C1",
}) {
const curves = { gentle: ["mot-spr-gentle", "cubic-bezier(.25,1.1,.4,1)"], bouncy: ["mot-spr-bouncy", "cubic-bezier(.34,1.56,.64,1)"], wobbly: ["mot-spr-wobbly", "cubic-bezier(.68,-0.6,.32,1.6)"] };
const [name, ease] = curves[bounce];
return (
<div style={{ width: "100%", maxWidth: 320, padding: 16, background: "#FAF7F2", border: "1px solid #24143022", borderRadius: 12, fontFamily: "system-ui, sans-serif", fontSize: 12, color: "#241430aa" }}>
<div style={{ position: "relative", height: 26 }}>
<span className={loop ? "dot loop" : "dot"} style={{ position: "absolute", top: 0, left: 1, width: 22, height: 22, borderRadius: 999, background: accent }} />
</div>
<p style={{ margin: "10px 0 0", textTransform: "capitalize" }}>{bounce} spring · overshoots then settles, unlike a plain ease</p>
<style>{`
@media (prefers-reduced-motion: no-preference) {
.dot { animation: ${name} ${duration}ms ${ease} 1 both; }
.dot.loop { animation-iteration-count: infinite; }
@keyframes mot-spr-gentle { from { left: 1px; } to { left: 260px; } }
@keyframes mot-spr-bouncy { from { left: 1px; } to { left: 260px; } }
@keyframes mot-spr-wobbly { from { left: 1px; } to { left: 260px; } }
}
`}</style>
</div>
);
}