Handbrake
import { useState } from 'react';
const css = `
@keyframes toggle-handbrake-ratchet {
0% { transform: rotate(0deg); }
12% { transform: rotate(-7deg); } 22% { transform: rotate(-6deg); }
34% { transform: rotate(-13deg); } 44% { transform: rotate(-12deg); }
56% { transform: rotate(-19deg); } 66% { transform: rotate(-18deg); }
80% { transform: rotate(-25deg); } 90%, 100% { transform: rotate(-24deg); }
}
@keyframes toggle-handbrake-press { 0%, 100% { transform: translateX(0); } 30%, 60% { transform: translateX(-2.5px); } }
.toggle-handbrake { display: inline-flex; align-items: center; gap: 14px; color: light-dark(#42454c, #e5e6e9); font-size: 15px; cursor: pointer; user-select: none; }
.toggle-handbrake__input { position: absolute; width: 1px; height: 1px; margin: 0; opacity: 0; }
.toggle-handbrake__art { display: block; width: 112px; height: 72px; overflow: visible; border-radius: 8px; }
.toggle-handbrake__input:focus-visible + .toggle-handbrake__art { outline: 2px solid light-dark(#5f626a, #a7abb2); outline-offset: 4px; }
.toggle-handbrake__console { fill: light-dark(#ebedf0, #232529); }
.toggle-handbrake__slot { fill: light-dark(#f3f4f7, #1a1c1f); }
.toggle-handbrake__teeth { fill: light-dark(#cbced3, #393c41); }
.toggle-handbrake__rod { fill: light-dark(#6d7179, #7d8087); }
.toggle-handbrake__grip { fill: light-dark(#cbced3, #393c41); }
.toggle-handbrake__ridge { stroke: light-dark(#dbdde1, #2e3035); stroke-width: 1.2; stroke-linecap: round; }
.toggle-handbrake__button { fill: light-dark(#50535b, #c6c8cd); }
.toggle-handbrake__hub { fill: light-dark(#5f626a, #a7abb2); }
.toggle-handbrake__lamp { fill: light-dark(#dbdde1, #2e3035); transition: fill 0.2s, filter 0.2s; }
.toggle-handbrake__lever { transform-box: view-box; transform-origin: 22px 50px; transition: transform 0.35s cubic-bezier(0.5, 0, 0.7, 1) 0.15s; }
/* Pulled on, it comes up a tooth at a time; let off, the button goes in and the lever drops. */
.toggle-handbrake__input:checked + .toggle-handbrake__art .toggle-handbrake__lever { transform: rotate(-24deg); animation: toggle-handbrake-ratchet 0.75s ease-out; }
.toggle-handbrake__input:checked + .toggle-handbrake__art .toggle-handbrake__lamp { fill: light-dark(#373a40, #f1f2f3); filter: drop-shadow(0 0 2.5px light-dark(#373a40, #f1f2f3)); transition-delay: 0.6s; }
.toggle-handbrake[data-armed] .toggle-handbrake__input:not(:checked) + .toggle-handbrake__art .toggle-handbrake__button { animation: toggle-handbrake-press 0.4s ease-in-out; }
@media (prefers-reduced-motion: reduce) {
.toggle-handbrake__lever { transition: none; }
.toggle-handbrake__input:checked + .toggle-handbrake__art .toggle-handbrake__lever,
.toggle-handbrake[data-armed] .toggle-handbrake__input:not(:checked) + .toggle-handbrake__art .toggle-handbrake__button { animation: none; }
}
`;
export function HandbrakeToggle() {
// The release press only plays once you have pulled it on at least once.
const [armed, setArmed] = useState(false);
return (
<>
<style>
{css}
</style>
<label className="toggle-handbrake" data-armed={armed || undefined}>
<input className="toggle-handbrake__input" type="checkbox" role="switch" onChange={() => setArmed(true)} />
<svg className="toggle-handbrake__art" viewBox="0 0 108 70" aria-hidden="true">
<rect className="toggle-handbrake__console" x="4" y="46" width="100" height="18" rx="7" />
<rect className="toggle-handbrake__slot" x="14" y="51" width="78" height="5" rx="2.5" />
<path className="toggle-handbrake__teeth" d="M14 46L16 42L18 46L20 41.5L22 46L24 41.5L26 46L28 42L30 46Z" />
<circle className="toggle-handbrake__lamp" cx="96" cy="58" r="2.2" />
<g className="toggle-handbrake__lever">
<path className="toggle-handbrake__rod" d="M18 47L80 43Q84 43 84 47Q84 51 80 51L18 53Q14 53 14 50Q14 47 18 47Z" />
<rect className="toggle-handbrake__grip" x="58" y="40.5" width="34" height="13" rx="6.5" />
<path className="toggle-handbrake__ridge" d="M64 43.5V50.5M69 43.5V50.5M74 43.5V50.5M79 43.5V50.5" />
<rect className="toggle-handbrake__button" x="91" y="43.5" width="5" height="7" rx="2" />
</g>
<circle className="toggle-handbrake__hub" cx="22" cy="50" r="3" />
</svg>
<span>Hold</span>
</label>
</>
);
}Markup where the component goes, CSS in your stylesheet, script before </body>. In Vue, Svelte or Angular, call initToggleHandbrake(el) on the mounted element instead.
<label class="toggle-handbrake">
<input class="toggle-handbrake__input" type="checkbox" role="switch"/>
<svg class="toggle-handbrake__art" viewBox="0 0 108 70" aria-hidden="true">
<rect class="toggle-handbrake__console" x="4" y="46" width="100" height="18" rx="7"></rect>
<rect class="toggle-handbrake__slot" x="14" y="51" width="78" height="5" rx="2.5"></rect>
<path class="toggle-handbrake__teeth" d="M14 46L16 42L18 46L20 41.5L22 46L24 41.5L26 46L28 42L30 46Z"></path>
<circle class="toggle-handbrake__lamp" cx="96" cy="58" r="2.2"></circle>
<g class="toggle-handbrake__lever">
<path class="toggle-handbrake__rod" d="M18 47L80 43Q84 43 84 47Q84 51 80 51L18 53Q14 53 14 50Q14 47 18 47Z"></path>
<rect class="toggle-handbrake__grip" x="58" y="40.5" width="34" height="13" rx="6.5"></rect>
<path class="toggle-handbrake__ridge" d="M64 43.5V50.5M69 43.5V50.5M74 43.5V50.5M79 43.5V50.5"></path>
<rect class="toggle-handbrake__button" x="91" y="43.5" width="5" height="7" rx="2"></rect>
</g>
<circle class="toggle-handbrake__hub" cx="22" cy="50" r="3"></circle>
</svg>
<span>Hold</span>
</label>@keyframes toggle-handbrake-ratchet {
0% { transform: rotate(0deg); }
12% { transform: rotate(-7deg); } 22% { transform: rotate(-6deg); }
34% { transform: rotate(-13deg); } 44% { transform: rotate(-12deg); }
56% { transform: rotate(-19deg); } 66% { transform: rotate(-18deg); }
80% { transform: rotate(-25deg); } 90%, 100% { transform: rotate(-24deg); }
}
@keyframes toggle-handbrake-press { 0%, 100% { transform: translateX(0); } 30%, 60% { transform: translateX(-2.5px); } }
.toggle-handbrake { display: inline-flex; align-items: center; gap: 14px; color: light-dark(#42454c, #e5e6e9); font-size: 15px; cursor: pointer; user-select: none; }
.toggle-handbrake__input { position: absolute; width: 1px; height: 1px; margin: 0; opacity: 0; }
.toggle-handbrake__art { display: block; width: 112px; height: 72px; overflow: visible; border-radius: 8px; }
.toggle-handbrake__input:focus-visible + .toggle-handbrake__art { outline: 2px solid light-dark(#5f626a, #a7abb2); outline-offset: 4px; }
.toggle-handbrake__console { fill: light-dark(#ebedf0, #232529); }
.toggle-handbrake__slot { fill: light-dark(#f3f4f7, #1a1c1f); }
.toggle-handbrake__teeth { fill: light-dark(#cbced3, #393c41); }
.toggle-handbrake__rod { fill: light-dark(#6d7179, #7d8087); }
.toggle-handbrake__grip { fill: light-dark(#cbced3, #393c41); }
.toggle-handbrake__ridge { stroke: light-dark(#dbdde1, #2e3035); stroke-width: 1.2; stroke-linecap: round; }
.toggle-handbrake__button { fill: light-dark(#50535b, #c6c8cd); }
.toggle-handbrake__hub { fill: light-dark(#5f626a, #a7abb2); }
.toggle-handbrake__lamp { fill: light-dark(#dbdde1, #2e3035); transition: fill 0.2s, filter 0.2s; }
.toggle-handbrake__lever { transform-box: view-box; transform-origin: 22px 50px; transition: transform 0.35s cubic-bezier(0.5, 0, 0.7, 1) 0.15s; }
/* Pulled on, it comes up a tooth at a time; let off, the button goes in and the lever drops. */
.toggle-handbrake__input:checked + .toggle-handbrake__art .toggle-handbrake__lever { transform: rotate(-24deg); animation: toggle-handbrake-ratchet 0.75s ease-out; }
.toggle-handbrake__input:checked + .toggle-handbrake__art .toggle-handbrake__lamp { fill: light-dark(#373a40, #f1f2f3); filter: drop-shadow(0 0 2.5px light-dark(#373a40, #f1f2f3)); transition-delay: 0.6s; }
.toggle-handbrake[data-armed] .toggle-handbrake__input:not(:checked) + .toggle-handbrake__art .toggle-handbrake__button { animation: toggle-handbrake-press 0.4s ease-in-out; }
@media (prefers-reduced-motion: reduce) {
.toggle-handbrake__lever { transition: none; }
.toggle-handbrake__input:checked + .toggle-handbrake__art .toggle-handbrake__lever,
.toggle-handbrake[data-armed] .toggle-handbrake__input:not(:checked) + .toggle-handbrake__art .toggle-handbrake__button { animation: none; }
}function initToggleHandbrake(root) {
// The motion keyframes only run once the control has been used, never on page load.
function onChange() {
root.setAttribute('data-armed', '');
}
root.addEventListener('change', onChange);
return function () {
root.removeEventListener('change', onChange);
};
}
document.querySelectorAll('.toggle-handbrake').forEach(initToggleHandbrake);Goal: Create a "Handbrake" toggle switch.
Visual behavior: A hold switch drawn as a handbrake: the lever ratchets up a click at a time; off presses the release button and the lever drops back down.
Technique:
- A native checkbox with role="switch", visually hidden but focusable, sits inside a <label> before an aria-hidden drawing and the visible text. Every on state is styled from the input's :checked state through the adjacent sibling combinator, so Space and clicks both work.
- Pulling on is a stepped keyframe that lifts the lever a tooth at a time; letting off presses the button and drops the lever with a transition.
- Keyframes that must not play on page load hang off a data-armed attribute set on the first change.
Accessibility: A native checkbox with role="switch" inside a <label>, so Space toggles it and the visible text names it; :focus-visible on the input outlines the drawing, which is aria-hidden. Under prefers-reduced-motion the lever moves at once.
My stack: {{USER_STACK}}
My styling: {{USER_STYLING}}
My constraints: {{USER_CONSTRAINTS}}
My colors: neutral only. Black, white and grays, no hues. Use the color values above as written, or my project's closest neutral tokens.
Return a single self-contained component. No dependencies.Goal: Create a "Handbrake" toggle switch.
Visual behavior: A hold switch drawn as a handbrake: the lever ratchets up a click at a time; off presses the release button and the lever drops back down.
Technique:
- A native checkbox with role="switch", visually hidden but focusable, sits inside a <label> before an aria-hidden drawing and the visible text. Every on state is styled from the input's :checked state through the adjacent sibling combinator, so Space and clicks both work.
- Pulling on is a stepped keyframe that lifts the lever a tooth at a time; letting off presses the button and drops the lever with a transition.
- Keyframes that must not play on page load hang off a data-armed attribute set on the first change.
Accessibility: A native checkbox with role="switch" inside a <label>, so Space toggles it and the visible text names it; :focus-visible on the input outlines the drawing, which is aria-hidden. Under prefers-reduced-motion the lever moves at once.
My stack: {{USER_STACK}}
My styling: {{USER_STYLING}}
My constraints: {{USER_CONSTRAINTS}}
My colors: use my project's existing color tokens. The color values above are grayscale placeholders. Map each one by its role (surface, text, border, accent, glow) to my theme, and keep their relative lightness so contrast and the effect still read.
Return a single self-contained component. No dependencies.Paste into Claude, ChatGPT or Cursor after filling in {{USER_STACK}}, {{USER_STYLING}} and {{USER_CONSTRAINTS}}. Neutral keeps the greys you see here; Match my project asks the model to map them onto your own colour tokens.
Free to use in any project, commercial included. MIT-0, no attribution required. Licence
About this component
A hold switch drawn as a handbrake: the lever ratchets up a click at a time; off presses the release button and the lever drops back down.
- Dependencies
- None
- Difficulty
- Intermediate
- Triggers
- click, state change, keyframe
- Techniques
- keyframes, css transition, svg
- Reduced motion
- Respected
- Theme
- Light and dark (follows your page's color-scheme)
A native checkbox with role="switch" inside a <label>, so Space toggles it and the visible text names it; :focus-visible on the input outlines the drawing, which is aria-hidden. Under prefers-reduced-motion the lever moves at once.