16 CSS Bounce Animations10 / 16

Pure CSSMIT licensed

Toggle Switch Elastic Bounce

A dark mode toggle switch with elastic bounce — the knob slides across and "lands" with a spring squash at the edge, mimicking a physical switch snapping into position. iOS Settings / Figma toolbar / GitHub theme picker aesthetic. Pure CSS via <input type="checkbox"> + :checked — no JavaScript needed. The bounce reads as "switch has snapped, state is committed."

Published

Live Demo
Try it

The code

<div class="bn-10">
  <p class="bn-10__label">Click to toggle</p>
  <label class="bn-10__toggle">
    <input type="checkbox" class="bn-10__input" checked />
    <span class="bn-10__track" aria-hidden="true">
      <span class="bn-10__knob"></span>
    </span>
    <span class="bn-10__text">Dark mode</span>
  </label>
  <p class="bn-10__hint">The knob undershoots, then overshoots + squashes as it lands.</p>
</div>
.bn-10 {
  --page-bg: #0f172a;
  --surface: #1e293b;
  --ink: #f1f5f9;
  --sub: #94a3b8;
  --track-off: #334155;
  --track-on: #8b5cf6;
  --knob: #ffffff;
  --knob-shadow: rgba(139,92,246,.4);
  font-family: -apple-system,BlinkMacSystemFont,'Inter','Segoe UI',sans-serif;
  width: 100%;
  min-height: 100vh;
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 24px;
  padding: 32px;
  background: radial-gradient(60% 60% at 50% 50%, rgba(139,92,246,.14) 0%, transparent 60%), var(--page-bg);
  color: var(--ink);
  text-align: center;
}

.bn-10 *,
.bn-10 *::before,
.bn-10 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.bn-10 ::selection {
  background: var(--track-on);
  color: #fff;
}

.bn-10__label {
  font-size: .9rem;
  font-weight: 600;
  color: var(--sub);
  letter-spacing: .02em;
}

.bn-10__toggle {
  display: inline-flex;
  align-items: center;
  gap: 14px;
  padding: 16px 28px;
  border-radius: 14px;
  background: var(--surface);
  cursor: pointer;
  box-shadow: 0 12px 32px -12px rgba(0,0,0,.4);
  min-height: 44px;
}

.bn-10__input {
  position: absolute;
  opacity: 0;
  pointer-events: none;
  width: 0;
  height: 0;
}

.bn-10__track {
  position: relative;
  display: inline-block;
  width: 60px;
  height: 32px;
  border-radius: 16px;
  background: var(--track-off);
  transition: background .38s cubic-bezier(.55,0,.45,1);
  flex-shrink: 0;
}

.bn-10__knob {
  position: absolute;
  top: 3px;
  left: 3px;
  width: 26px;
  height: 26px;
  border-radius: 50%;
  background: var(--knob);
  box-shadow: 0 4px 10px -2px rgba(0,0,0,.4),0 0 0 0 var(--knob-shadow);
  transition: transform .38s cubic-bezier(.68,-0.6,.32,1.6),box-shadow .38s ease;
  will-change: transform;
}

.bn-10__input:checked ~ .bn-10__track {
  background: var(--track-on);
}

.bn-10__input:checked ~ .bn-10__track .bn-10__knob {
  transform: translateX(28px);
  box-shadow: 0 4px 10px -2px rgba(0,0,0,.4),0 0 12px 2px var(--knob-shadow);
  animation: bn-10-squash .35s cubic-bezier(.34,1.56,.64,1);
}

@keyframes bn-10-squash {
  0% {
    transform: translateX(28px) scale(1);
  }

  60% {
    transform: translateX(28px) scaleX(1.15) scaleY(.85);
  }

  100% {
    transform: translateX(28px) scale(1);
  }
}

.bn-10__text {
  font-size: .95rem;
  font-weight: 700;
  letter-spacing: -.005em;
}

.bn-10__toggle:focus-within {
  outline: 2px solid var(--track-on);
  outline-offset: 4px;
}

.bn-10__hint {
  font-size: .78rem;
  color: var(--sub);
  max-width: 360px;
  line-height: 1.5;
}

@media (prefers-reduced-motion: reduce) {
  .bn-10__knob {
    transition: transform .25s cubic-bezier(.22,1,.36,1)!important;
    animation: none!important;
  }

  .bn-10__input:checked ~ .bn-10__track .bn-10__knob {
    animation: none!important;
  }
}
Paste this into ChatGPT, Claude, Cursor, or any coding assistant. The block below is pre-framed with everything the AI needs to integrate this demo into your project — markup, styles, scoping notes, and the source URL. Hit Copy and paste straight into your chat.
Here's a working CSS Bounce Animation from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Toggle Switch Elastic Bounce
Source: https://codefronts.com/motion/css-bounce-animation/toggle-switch-elastic-bounce/

A dark mode toggle switch with elastic bounce — the knob slides across and "lands" with a spring squash at the edge, mimicking a physical switch snapping into position. iOS Settings / Figma toolbar / GitHub theme picker aesthetic. Pure CSS via <input type="checkbox"> + :checked — no JavaScript needed. The bounce reads as "switch has snapped, state is committed."
## HTML
```html
<div class="bn-10">
  <p class="bn-10__label">Click to toggle</p>
  <label class="bn-10__toggle">
    <input type="checkbox" class="bn-10__input" checked />
    <span class="bn-10__track" aria-hidden="true">
      <span class="bn-10__knob"></span>
    </span>
    <span class="bn-10__text">Dark mode</span>
  </label>
  <p class="bn-10__hint">The knob undershoots, then overshoots + squashes as it lands.</p>
</div>
```
## CSS
```css
.bn-10 {
  --page-bg: #0f172a;
  --surface: #1e293b;
  --ink: #f1f5f9;
  --sub: #94a3b8;
  --track-off: #334155;
  --track-on: #8b5cf6;
  --knob: #ffffff;
  --knob-shadow: rgba(139,92,246,.4);
  font-family: -apple-system,BlinkMacSystemFont,'Inter','Segoe UI',sans-serif;
  width: 100%;
  min-height: 100vh;
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 24px;
  padding: 32px;
  background: radial-gradient(60% 60% at 50% 50%, rgba(139,92,246,.14) 0%, transparent 60%), var(--page-bg);
  color: var(--ink);
  text-align: center;
}

.bn-10 *,
.bn-10 *::before,
.bn-10 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.bn-10 ::selection {
  background: var(--track-on);
  color: #fff;
}

.bn-10__label {
  font-size: .9rem;
  font-weight: 600;
  color: var(--sub);
  letter-spacing: .02em;
}

.bn-10__toggle {
  display: inline-flex;
  align-items: center;
  gap: 14px;
  padding: 16px 28px;
  border-radius: 14px;
  background: var(--surface);
  cursor: pointer;
  box-shadow: 0 12px 32px -12px rgba(0,0,0,.4);
  min-height: 44px;
}

.bn-10__input {
  position: absolute;
  opacity: 0;
  pointer-events: none;
  width: 0;
  height: 0;
}

.bn-10__track {
  position: relative;
  display: inline-block;
  width: 60px;
  height: 32px;
  border-radius: 16px;
  background: var(--track-off);
  transition: background .38s cubic-bezier(.55,0,.45,1);
  flex-shrink: 0;
}

.bn-10__knob {
  position: absolute;
  top: 3px;
  left: 3px;
  width: 26px;
  height: 26px;
  border-radius: 50%;
  background: var(--knob);
  box-shadow: 0 4px 10px -2px rgba(0,0,0,.4),0 0 0 0 var(--knob-shadow);
  transition: transform .38s cubic-bezier(.68,-0.6,.32,1.6),box-shadow .38s ease;
  will-change: transform;
}

.bn-10__input:checked ~ .bn-10__track {
  background: var(--track-on);
}

.bn-10__input:checked ~ .bn-10__track .bn-10__knob {
  transform: translateX(28px);
  box-shadow: 0 4px 10px -2px rgba(0,0,0,.4),0 0 12px 2px var(--knob-shadow);
  animation: bn-10-squash .35s cubic-bezier(.34,1.56,.64,1);
}

@keyframes bn-10-squash {
  0% {
    transform: translateX(28px) scale(1);
  }

  60% {
    transform: translateX(28px) scaleX(1.15) scaleY(.85);
  }

  100% {
    transform: translateX(28px) scale(1);
  }
}

.bn-10__text {
  font-size: .95rem;
  font-weight: 700;
  letter-spacing: -.005em;
}

.bn-10__toggle:focus-within {
  outline: 2px solid var(--track-on);
  outline-offset: 4px;
}

.bn-10__hint {
  font-size: .78rem;
  color: var(--sub);
  max-width: 360px;
  line-height: 1.5;
}

@media (prefers-reduced-motion: reduce) {
  .bn-10__knob {
    transition: transform .25s cubic-bezier(.22,1,.36,1)!important;
    animation: none!important;
  }

  .bn-10__input:checked ~ .bn-10__track .bn-10__knob {
    animation: none!important;
  }
}
```

How this works

A hidden <input type="checkbox"> drives the visual state via CSS :checked. The knob (a <span> inside the label) animates its transform: translateX from 0 to 28px (or wherever the right edge is) when checked. The bounce comes from the timing function: transition: transform .38s cubic-bezier(.68, -0.6, .32, 1.6). The negative first Y coordinate (-0.6) creates an undershoot — the knob briefly moves backwards before shooting forward — and the second Y coordinate (1.6) creates the overshoot as the knob "lands" at the edge.

Simultaneously, the knob does a scale-squash at the moment of "landing" via a stacked animation on :checked ~ .knob: a brief @keyframes bn-10-squash that scales scaleX(1.15) scaleY(.85) at 60% then settles to scale(1) at 100%. This 100ms squash mimics the compression of a rubber puck hitting a wall. The track color shifts from gray to accent purple simultaneously so the state change reads as coherent (color + position + squash all happen together).

The demo shows the toggle as the hero at 60×32px with a "Dark mode" label beside it. On :checked, both the knob position AND the label context update to reinforce the state change.

Make it yours

  • Change the elastic intensity by editing cubic-bezier(.68, -0.6, .32, 1.6) — first Y (-0.6) is undershoot, second Y (1.6) is overshoot. -0.3 / 1.3 reads as subtle; -1 / 2.0 reads as goofy (avoid — feels broken).
  • Adjust the knob travel distance by editing translateX(28px) — smaller for compact toggles (iOS Settings 24px), larger for accessibility-first designs (44px).
  • For touch-device-safe sizing, ensure the label wrapper hits the 44×44px WCAG 2.5.5 minimum tap target — pad the label around the visual toggle even if the toggle itself is smaller.
  • For multi-state toggles (Off / On / Auto), swap the checkbox for a <fieldset> with three radio inputs + :checked-driven positions at 0px / 28px / 56px.
  • For Tailwind: use peer + peer-checked:translate-x-7 + arbitrary transition transition-[transform] duration-[380ms] ease-[cubic-bezier(.68,-0.6,.32,1.6)]. See FAQ #4.

Gotchas — read before shipping

  • The elastic cubic-bezier's negative first Y (undershoot) can look glitchy on very short travel distances (under 16px) because the backwards motion is proportionally huge. Use it on 24px+ travel; for shorter, use a simpler overshoot-only curve like cubic-bezier(.34, 1.56, .64, 1).
  • For accessibility, use a REAL <input type="checkbox"> with visually-hidden styling — not a fake <div role="switch">. Real inputs get Space-key toggle + Tab focus + form submission for free. Fake ones require JS + ARIA and still fail edge cases.
  • The visually-hidden input MUST keep opacity: 0 (not display: none) — display: none removes the element from the accessibility tree and screen readers skip it entirely.
  • For color-blind users, DO NOT rely on color alone (gray vs purple track) to convey state. The knob position IS a redundant signal (left = off, right = on), which is why toggle switches work for color-blind users. Never build a toggle where only color changes.
  • For iOS Safari, add -webkit-appearance: none to the input to strip the default checkbox chrome, otherwise you'll see a native checkbox faintly behind your custom track.
  • For platforms where users can rapidly toggle (dark mode preview), the elastic bounce should NOT stack. If the user clicks 5 times in a second, you don't want 5 overlapping animations. CSS transition handles this automatically — the current transition is interrupted and restarted from the current position.
  • For prefers-reduced-motion, replace the elastic cubic-bezier with a straight cubic-bezier(.22, 1, .36, 1) (smooth deceleration, no bounce). The state change still happens — knob still moves — but decorative elastic motion is disabled.

Browser support

ChromeSafariFirefoxEdge
45+10+40+45+

checkbox + :checked + cubic-bezier + transform. Universal, GPU-accelerated.

Techniques used in this demo

Search CodeFronts

Loading…