Spring Animation

Physics-based motion with natural overshoot.

bouncy spring · overshoots then settles, unlike a plain ease

html

<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 &middot; 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>

tailwind

<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 &middot; 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>

react

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 &middot; 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>
  );
}