Signal

No dependencies
Code format
React + Tailwind
const css = `
  .toggle-signal { display: inline-flex; align-items: center; gap: 14px; color: light-dark(#42454c, #e5e6e9); font-size: 15px; cursor: pointer; user-select: none; }
  .toggle-signal__input { position: absolute; width: 1px; height: 1px; margin: 0; opacity: 0; }
  .toggle-signal__art { display: block; width: 72px; height: 96px; overflow: visible; border-radius: 8px; }
  .toggle-signal__input:focus-visible + .toggle-signal__art { outline: 2px solid light-dark(#5f626a, #a7abb2); outline-offset: 4px; }
  .toggle-signal__ground { fill: light-dark(#dbdde1, #2e3035); }
  .toggle-signal__post { fill: light-dark(#cbced3, #393c41); }
  .toggle-signal__rung { fill: none; stroke: light-dark(#dbdde1, #2e3035); stroke-width: 1; }
  .toggle-signal__housing { fill: light-dark(#dbdde1, #2e3035); }
  .toggle-signal__blade { fill: light-dark(#42454c, #e5e6e9); }
  .toggle-signal__band { fill: light-dark(#dbdde1, #2e3035); }
  .toggle-signal__glass { fill: light-dark(#ebedf0, #232529); stroke: light-dark(#6d7179, #7d8087); stroke-width: 1.5; }
  .toggle-signal__hub { fill: light-dark(#5f626a, #a7abb2); }
  .toggle-signal__lamp { fill: light-dark(#cbced3, #393c41); transition: fill 0.2s, filter 0.2s; }
  /* Pulled off, the arm drops under its own weight and bounces on its stop; set back, it is hauled up. */
  .toggle-signal__arm { transform-box: view-box; transform-origin: 34px 20px; transition: transform 0.5s cubic-bezier(0.4, 0, 0.3, 1); }
  .toggle-signal__input:checked + .toggle-signal__art .toggle-signal__arm { transform: rotate(42deg); transition: transform 0.55s cubic-bezier(0.45, 1.75, 0.5, 1); }
  .toggle-signal__input:checked + .toggle-signal__art .toggle-signal__lamp { fill: light-dark(#373a40, #f1f2f3); filter: drop-shadow(0 0 3px light-dark(#373a40, #f1f2f3)); transition-delay: 0.15s; }
  @media (prefers-reduced-motion: reduce) {
    .toggle-signal__arm, .toggle-signal__input:checked + .toggle-signal__art .toggle-signal__arm { transition: none; }
  }
`;

export function SignalToggle() {
  return (
    <>
      <style>
        {css}
      </style>
      <label className="toggle-signal">
        <input className="toggle-signal__input" type="checkbox" role="switch" />
        <svg className="toggle-signal__art" viewBox="0 0 66 88" aria-hidden="true">
          <rect className="toggle-signal__ground" x="16" y="82" width="34" height="4" rx="2" />
          <path className="toggle-signal__post" d="M31 82V10L32.5 4L34 10V82Z" />
          <path className="toggle-signal__rung" d="M27 76H31M27 68H31M27 60H31M27 52H31M27 44H31M27 36H31M27 76V36" />
          <rect className="toggle-signal__housing" x="20" y="26" width="11" height="12" rx="2.5" />
          <circle className="toggle-signal__lamp" cx="25.5" cy="32" r="3" />
          <g className="toggle-signal__arm">
            {/* The counterweight on the short end, past the pivot. */}
            <circle className="toggle-signal__glass" cx="27" cy="20" r="3.5" />
            <rect className="toggle-signal__blade" x="33" y="17" width="28" height="6" rx="1" />
            <rect className="toggle-signal__band" x="52" y="17" width="3" height="6" />
          </g>
          <circle className="toggle-signal__hub" cx="34" cy="20" r="2.5" />
        </svg>
        <span>Ready</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-signal
Source on GitHub

About this component

A ready switch drawn as a railway semaphore: the arm drops to clear under its own weight and bounces on its stop as the lamp lights; off hauls it back up.

Dependencies
None
Difficulty
Beginner
Triggers
click, state change, spring
Techniques
css transition, svg, transform only
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 arm moves at once.

More toggles

See all
Turntable
Valve
Wash
Broadcast