Interaction blueprint
Weightless toggle
A firm toggle with a deliberate settle. The distance is short and the state change is unmissable.
Build it
Choose a target and adapt the source to your interface.
<button class="toggle" type="button" role="switch" aria-checked="false">
<span class="toggle__track" aria-hidden="true"
><span class="toggle__thumb"></span
></span>
Notifications
</button>
<script>
const toggle = document.querySelector('.toggle');
toggle.addEventListener('click', () =>
toggle.setAttribute(
'aria-checked',
String(toggle.getAttribute('aria-checked') !== 'true'),
),
);
</script>
<style>
.toggle {
display: inline-flex;
align-items: center;
gap: 12px;
min-height: 44px;
padding: 6px;
border: 0;
background: transparent;
color: inherit;
cursor: pointer;
}
.toggle__track {
display: block;
width: 56px;
height: 32px;
padding: 4px;
border-radius: 99px;
background: #536174;
transition: background 240ms ease-out;
}
.toggle__thumb {
display: block;
width: 24px;
height: 24px;
border-radius: 50%;
background: white;
transform: translateX(0);
transition: transform 240ms cubic-bezier(0.4, 0, 0.2, 1);
}
.toggle[aria-checked='true'] .toggle__track {
background: #175bc5;
}
.toggle[aria-checked='true'] .toggle__thumb {
transform: translateX(24px);
}
.toggle:focus-visible {
outline: 2px solid #175bc5;
outline-offset: 4px;
border-radius: 6px;
}
@media (prefers-reduced-motion: reduce) {
.toggle__track,
.toggle__thumb {
transition: none;
}
}
</style>Animation breakdown
Frame phases, motion intent, implementation constraints and accessible alternatives.
Rest: knob rests left.
0–160ms: it crosses the track.
160–240ms: it settles on the far edge as the track changes colour.
Motion intent
A firm toggle with a deliberate settle. The distance is short and the state change is unmissable.
This duration leaves enough time to read the state change while keeping the interface responsive.
The fast acceleration and long ease-out make the state feel responsive while giving the final pixels time to settle.
Motion specification
- Trigger
- A binary preference changes immediately.
- Timing
- 240ms
- Easing
- cubic-bezier(.4, 0, .2, 1)
Animated properties
Implementation notes
- Keep off and on aligned to the same visual anchor.
- Drive the transition from semantic state, not a timer, when the trigger is interactive.
- Use translate, background-color, shadow and avoid layout work inside the animation loop.
Reduced motion
Swap to the final state instantly, preserving colour, label, and accessible state.
- Use a minimum 44 by 44 pixel interactive target.
- Expose the off and on states with aria-pressed, aria-expanded, or an updated accessible label.
- Keep visible keyboard focus independent from the animation.
Avoid
- Do not add bounce unless a real object pickup or spring settle is part of the interaction.
- Do not hide the final state from assistive technology.
- Do not make the user wait for animation before the underlying state updates.