Handbrake

No dependencies
Code format
React + Tailwind
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>
    </>
  );
}

Free to use in any project, commercial included. MIT-0, no attribution required. Licence

Install with shadcn

npx shadcn@latest add @movin/toggle-handbrake
Source on GitHub

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.

More toggles

See all
Jack
Kettle
Lights
Periscope