Bolt

No dependencies
Code format
React + Tailwind
import { useState } from 'react';

const css = `
  @keyframes toggle-bolt-clack { 0%, 100% { transform: translateX(0); } 30% { transform: translateX(1.5px); } 60% { transform: translateX(-0.6px); } }
  .toggle-bolt { display: inline-flex; align-items: center; gap: 14px; color: light-dark(#42454c, #e5e6e9); font-size: 15px; cursor: pointer; user-select: none; }
  .toggle-bolt__input { position: absolute; width: 1px; height: 1px; margin: 0; opacity: 0; }
  .toggle-bolt__art { display: block; width: 128px; height: 52px; overflow: visible; border-radius: 8px; }
  .toggle-bolt__input:focus-visible + .toggle-bolt__art { outline: 2px solid light-dark(#5f626a, #a7abb2); outline-offset: 4px; }
  .toggle-bolt__door { fill: light-dark(#ebedf0, #232529); }
  .toggle-bolt__plate { fill: light-dark(#cbced3, #393c41); }
  .toggle-bolt__screw { fill: light-dark(#6d7179, #7d8087); }
  .toggle-bolt__slot { fill: light-dark(#dbdde1, #2e3035); }
  .toggle-bolt__keeper { fill: light-dark(#cbced3, #393c41); }
  .toggle-bolt__hole { fill: light-dark(#f3f4f7, #1a1c1f); }
  .toggle-bolt__bar { fill: light-dark(#50535b, #c6c8cd); }
  .toggle-bolt__stem { fill: light-dark(#5f626a, #a7abb2); }
  .toggle-bolt__knob { fill: light-dark(#42454c, #e5e6e9); }
  /* Locking: slide across, then the knob turns down into its notch. Unlocking runs the other way. */
  .toggle-bolt__slide { transition: transform 0.4s cubic-bezier(0.5, 0, 0.3, 1) 0.2s; }
  .toggle-bolt__turn { transform-box: view-box; transform-origin: 31px 24px; transition: transform 0.2s ease-in; }
  .toggle-bolt__input:checked + .toggle-bolt__art .toggle-bolt__slide { transform: translateX(24px); transition: transform 0.4s cubic-bezier(0.6, 0, 0.8, 0.6); }
  .toggle-bolt__input:checked + .toggle-bolt__art .toggle-bolt__turn { transform: scaleY(-1); transition: transform 0.22s cubic-bezier(0.4, 0, 0.6, 1.4) 0.42s; }
  .toggle-bolt[data-armed] .toggle-bolt__input:checked + .toggle-bolt__art .toggle-bolt__frame { animation: toggle-bolt-clack 0.25s ease-out 0.38s; }
  @media (prefers-reduced-motion: reduce) {
    .toggle-bolt__slide, .toggle-bolt__turn, .toggle-bolt__input:checked + .toggle-bolt__art .toggle-bolt__slide, .toggle-bolt__input:checked + .toggle-bolt__art .toggle-bolt__turn { transition: none; }
    .toggle-bolt[data-armed] .toggle-bolt__input:checked + .toggle-bolt__art .toggle-bolt__frame { animation: none; }
  }
`;

export function BoltToggle() {
  // The keeper only jolts once someone has slid the bolt, never on load.
  const [armed, setArmed] = useState(false);
  return (
    <>
      <style>
        {css}
      </style>
      <label className="toggle-bolt" data-armed={armed || undefined}>
        <input className="toggle-bolt__input" type="checkbox" role="switch" onChange={() => setArmed(true)} />
        <svg className="toggle-bolt__art" viewBox="0 0 124 50" aria-hidden="true">
          <rect className="toggle-bolt__door" x="2" y="6" width="84" height="38" rx="3" />
          <g className="toggle-bolt__frame">
            <rect className="toggle-bolt__door" x="89" y="6" width="33" height="38" rx="3" />
            <rect className="toggle-bolt__keeper" x="93" y="16" width="20" height="16" rx="3" />
            <rect className="toggle-bolt__hole" x="93" y="21" width="15" height="6" rx="1.5" />
          </g>
          <rect className="toggle-bolt__plate" x="12" y="14" width="56" height="20" rx="3" />
          {[
            [16, 18],
            [64, 18],
            [16, 30],
            [64, 30],
          ].map(([x, y]) => (
            <circle key={`${x}-${y}`} className="toggle-bolt__screw" cx={x} cy={y} r="1.6" />
          ))}
          {/* The slot the knob rides in, with the notch it drops into at the locked end. */}
          <path className="toggle-bolt__slot" d="M24 15H58V18H24ZM52 30H58V33H52Z" />
          <g className="toggle-bolt__slide">
            <rect className="toggle-bolt__bar" x="20" y="21.5" width="56" height="5" rx="2.5" />
            <g className="toggle-bolt__turn">
              <rect className="toggle-bolt__stem" x="29.5" y="12" width="3" height="11" rx="1" />
              <circle className="toggle-bolt__knob" cx="31" cy="11" r="4" />
            </g>
          </g>
        </svg>
        <span>Lock</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-bolt
Source on GitHub

About this component

A lock switch drawn as a door bolt: the bolt slides across into the keeper and its knob turns down into the notch with a clack; unlocking runs the other way.

Dependencies
None
Difficulty
Beginner
Triggers
click, state change
Techniques
css transition, keyframes, 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 bolt moves at once.

More toggles

See all
Bubbler
Burner
Chess clock
Drawbridge