Valve

No dependencies
Code format
React + Tailwind
const css = `
  @keyframes toggle-valve-twitch { 0%, 100% { transform: rotate(0deg); } 25% { transform: rotate(3deg); } 60% { transform: rotate(-2deg); } }
  @keyframes toggle-valve-flow { to { stroke-dashoffset: -16; } }
  .toggle-valve { display: inline-flex; align-items: center; gap: 14px; color: light-dark(#42454c, #e5e6e9); font-size: 15px; cursor: pointer; user-select: none; }
  .toggle-valve__input { position: absolute; width: 1px; height: 1px; margin: 0; opacity: 0; }
  .toggle-valve__art { display: block; width: 128px; height: 76px; overflow: visible; border-radius: 8px; }
  .toggle-valve__input:focus-visible + .toggle-valve__art { outline: 2px solid light-dark(#5f626a, #a7abb2); outline-offset: 4px; }
  .toggle-valve__pipe { fill: light-dark(#cbced3, #393c41); }
  .toggle-valve__flange { fill: light-dark(#6d7179, #7d8087); }
  .toggle-valve__glass { fill: light-dark(#f3f4f7, #1a1c1f); stroke: light-dark(#6d7179, #7d8087); stroke-width: 1; }
  .toggle-valve__body { fill: light-dark(#6d7179, #7d8087); }
  .toggle-valve__stem { fill: light-dark(#5f626a, #a7abb2); }
  .toggle-valve__rim { fill: none; stroke: light-dark(#50535b, #c6c8cd); stroke-width: 3.5; }
  .toggle-valve__spoke { stroke: light-dark(#50535b, #c6c8cd); stroke-width: 2.4; stroke-linecap: round; }
  .toggle-valve__hub { fill: light-dark(#42454c, #e5e6e9); }
  .toggle-valve__dial { fill: light-dark(#f3f4f7, #1a1c1f); stroke: light-dark(#6d7179, #7d8087); stroke-width: 2; }
  .toggle-valve__scale { fill: none; stroke: light-dark(#cbced3, #393c41); stroke-width: 1.4; stroke-dasharray: 1.2 3.2; }
  .toggle-valve__red { fill: none; stroke: light-dark(#50535b, #c6c8cd); stroke-width: 1.4; }
  .toggle-valve__needle { stroke: light-dark(#373a40, #f1f2f3); stroke-width: 1.4; stroke-linecap: round; }
  .toggle-valve__pin { fill: light-dark(#373a40, #f1f2f3); }
  .toggle-valve__water { fill: none; stroke: light-dark(#50535b, #c6c8cd); stroke-width: 2.5; stroke-linecap: round; stroke-dasharray: 5 3; opacity: 0; transition: opacity 0.3s; }
  /* Open: the wheel spins a turn and a half, pressure builds on the gauge and the flow runs; closing reverses it. */
  .toggle-valve__wheel { transform-box: view-box; transform-origin: 36px 24px; transition: transform 0.9s cubic-bezier(0.45, 0, 0.35, 1); }
  .toggle-valve__gauge { transform-box: view-box; transform-origin: 96px 26px; transform: rotate(-120deg); transition: transform 0.6s ease-in 0.2s; }
  .toggle-valve__input:checked + .toggle-valve__art .toggle-valve__wheel { transform: rotate(-540deg); }
  .toggle-valve__input:checked + .toggle-valve__art .toggle-valve__gauge { transform: rotate(35deg); transition: transform 1s cubic-bezier(0.3, 1.4, 0.5, 1) 0.5s; }
  .toggle-valve__input:checked + .toggle-valve__art .toggle-valve__twitch { animation: toggle-valve-twitch 0.6s steps(3) 1.5s infinite; }
  .toggle-valve__twitch { transform-box: view-box; transform-origin: 96px 26px; }
  .toggle-valve__input:checked + .toggle-valve__art .toggle-valve__water { opacity: 1; animation: toggle-valve-flow 0.45s linear infinite; transition-delay: 0.6s; }
  @media (prefers-reduced-motion: reduce) {
    .toggle-valve__wheel, .toggle-valve__gauge, .toggle-valve__input:checked + .toggle-valve__art .toggle-valve__gauge { transition: none; }
    .toggle-valve__input:checked + .toggle-valve__art .toggle-valve__twitch, .toggle-valve__input:checked + .toggle-valve__art .toggle-valve__water { animation: none; }
  }
`;

export function ValveToggle() {
  return (
    <>
      <style>
        {css}
      </style>
      <label className="toggle-valve">
        <input className="toggle-valve__input" type="checkbox" role="switch" />
        <svg className="toggle-valve__art" viewBox="0 0 124 74" aria-hidden="true">
          <rect className="toggle-valve__pipe" x="0" y="50" width="124" height="12" />
          <rect className="toggle-valve__glass" x="70" y="50" width="34" height="12" />
          <line className="toggle-valve__water" x1="72" y1="56" x2="102" y2="56" />
          <rect className="toggle-valve__flange" x="66" y="47" width="5" height="18" rx="1" />
          <rect className="toggle-valve__flange" x="103" y="47" width="5" height="18" rx="1" />
          <rect className="toggle-valve__stem" x="34.5" y="24" width="3" height="20" />
          <path className="toggle-valve__body" d="M24 44H48V68Q48 70 46 70H26Q24 70 24 68Z" />
          <rect className="toggle-valve__flange" x="20" y="47" width="5" height="18" rx="1" />
          <rect className="toggle-valve__flange" x="47" y="47" width="5" height="18" rx="1" />
          <g className="toggle-valve__wheel">
            <circle className="toggle-valve__rim" cx="36" cy="24" r="15" />
            {[0, 60, 120].map((a) => (
              <line key={a} className="toggle-valve__spoke" x1="36" y1="10" x2="36" y2="38" transform={`rotate(${a} 36 24)`} />
            ))}
            <circle className="toggle-valve__hub" cx="36" cy="24" r="3.5" />
          </g>
          <rect className="toggle-valve__stem" x="94.5" y="36" width="3" height="14" />
          <circle className="toggle-valve__dial" cx="96" cy="26" r="13" />
          <path className="toggle-valve__scale" d="M86.3 31.6A11 11 0 1 1 105.7 31.6" />
          <path className="toggle-valve__red" d="M104.3 21A9.5 9.5 0 0 1 105.4 29" />
          <g className="toggle-valve__twitch">
            <g className="toggle-valve__gauge">
              <line className="toggle-valve__needle" x1="96" y1="28" x2="96" y2="16" />
            </g>
          </g>
          <circle className="toggle-valve__pin" cx="96" cy="26" r="1.6" />
        </svg>
        <span>Sync</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-valve
Source on GitHub

About this component

A sync switch drawn as a valve: the handwheel spins a turn and a half, the gauge needle climbs and twitches, and flow runs through the glass section of the pipe.

Dependencies
None
Difficulty
Intermediate
Triggers
click, state change, loop
Techniques
css transition, keyframes, stroke dashoffset, 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 wheel and needle move at once and the flow is still.

More toggles

See all
Wash
Broadcast
Door
Earbuds