Valve
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>
</>
);
}Markup where the component goes, CSS in your stylesheet. No JavaScript.
<label class="toggle-valve">
<input class="toggle-valve__input" type="checkbox" role="switch"/>
<svg class="toggle-valve__art" viewBox="0 0 124 74" aria-hidden="true">
<rect class="toggle-valve__pipe" x="0" y="50" width="124" height="12"></rect>
<rect class="toggle-valve__glass" x="70" y="50" width="34" height="12"></rect>
<line class="toggle-valve__water" x1="72" y1="56" x2="102" y2="56"></line>
<rect class="toggle-valve__flange" x="66" y="47" width="5" height="18" rx="1"></rect>
<rect class="toggle-valve__flange" x="103" y="47" width="5" height="18" rx="1"></rect>
<rect class="toggle-valve__stem" x="34.5" y="24" width="3" height="20"></rect>
<path class="toggle-valve__body" d="M24 44H48V68Q48 70 46 70H26Q24 70 24 68Z"></path>
<rect class="toggle-valve__flange" x="20" y="47" width="5" height="18" rx="1"></rect>
<rect class="toggle-valve__flange" x="47" y="47" width="5" height="18" rx="1"></rect>
<g class="toggle-valve__wheel">
<circle class="toggle-valve__rim" cx="36" cy="24" r="15"></circle>
<line class="toggle-valve__spoke" x1="36" y1="10" x2="36" y2="38" transform="rotate(0 36 24)"></line>
<line class="toggle-valve__spoke" x1="36" y1="10" x2="36" y2="38" transform="rotate(60 36 24)"></line>
<line class="toggle-valve__spoke" x1="36" y1="10" x2="36" y2="38" transform="rotate(120 36 24)"></line>
<circle class="toggle-valve__hub" cx="36" cy="24" r="3.5"></circle>
</g>
<rect class="toggle-valve__stem" x="94.5" y="36" width="3" height="14"></rect>
<circle class="toggle-valve__dial" cx="96" cy="26" r="13"></circle>
<path class="toggle-valve__scale" d="M86.3 31.6A11 11 0 1 1 105.7 31.6"></path>
<path class="toggle-valve__red" d="M104.3 21A9.5 9.5 0 0 1 105.4 29"></path>
<g class="toggle-valve__twitch">
<g class="toggle-valve__gauge">
<line class="toggle-valve__needle" x1="96" y1="28" x2="96" y2="16"></line>
</g>
</g>
<circle class="toggle-valve__pin" cx="96" cy="26" r="1.6"></circle>
</svg>
<span>Sync</span>
</label>@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; }
}Goal: Create a "Valve" toggle switch.
Visual behavior: 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.
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.
- The wheel turns 540 degrees with a transition and the gauge needle swings up the dial, then loops a small twitch.
- The flow is a dashed stroke whose dash offset loops, shown only while the valve is open. Pure CSS.
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 wheel and needle move at once and the flow is still.
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 "Valve" toggle switch.
Visual behavior: 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.
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.
- The wheel turns 540 degrees with a transition and the gauge needle swings up the dial, then loops a small twitch.
- The flow is a dashed stroke whose dash offset loops, shown only while the valve is open. Pure CSS.
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 wheel and needle move at once and the flow is still.
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 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.