16 CSS Bounce Animations10 / 16
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
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><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;
}
}.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;
}
}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;
}
}
```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.3reads as subtle;-1 / 2.0reads 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 transitiontransition-[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">withvisually-hiddenstyling — 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(notdisplay: none) —display: noneremoves 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: noneto 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
transitionhandles this automatically — the current transition is interrupted and restarted from the current position. - For
prefers-reduced-motion, replace the elasticcubic-bezierwith a straightcubic-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
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 45+ | 10+ | 40+ | 45+ |
checkbox + :checked + cubic-bezier + transform. Universal, GPU-accelerated.