Wash
import { useId } from 'react';
const css = `
@keyframes toggle-wash-spin { to { transform: rotate(360deg); } }
@keyframes toggle-wash-shake { 0%, 100% { transform: translate(0, 0); } 25% { transform: translate(0.5px, -0.3px); } 75% { transform: translate(-0.5px, 0.3px); } }
@keyframes toggle-wash-slosh { 0%, 100% { transform: rotate(-4deg); } 50% { transform: rotate(4deg); } }
.toggle-wash { display: inline-flex; align-items: center; gap: 14px; color: light-dark(#42454c, #e5e6e9); font-size: 15px; cursor: pointer; user-select: none; }
.toggle-wash__input { position: absolute; width: 1px; height: 1px; margin: 0; opacity: 0; }
.toggle-wash__art { display: block; width: 80px; height: 92px; overflow: visible; border-radius: 8px; }
.toggle-wash__input:focus-visible + .toggle-wash__art { outline: 2px solid light-dark(#5f626a, #a7abb2); outline-offset: 4px; }
.toggle-wash__body { fill: light-dark(#dbdde1, #2e3035); }
.toggle-wash__panel { fill: light-dark(#cbced3, #393c41); }
.toggle-wash__screen { fill: light-dark(#ebedf0, #232529); }
.toggle-wash__knob { fill: light-dark(#5f626a, #a7abb2); }
.toggle-wash__led { fill: light-dark(#6d7179, #7d8087); transition: fill 0.2s, filter 0.2s; }
.toggle-wash__ring { fill: light-dark(#6d7179, #7d8087); }
.toggle-wash__glass { fill: light-dark(#f3f4f7, #1a1c1f); }
.toggle-wash__water { fill: color-mix(in srgb, light-dark(#5f626a, #a7abb2) 55%, transparent); }
.toggle-wash__hole { fill: light-dark(#ebedf0, #232529); }
.toggle-wash__cloth { fill: light-dark(#50535b, #c6c8cd); }
.toggle-wash__cloth--b { fill: light-dark(#6d7179, #7d8087); }
.toggle-wash__shine { fill: none; stroke: color-mix(in srgb, light-dark(#373a40, #f1f2f3) 22%, transparent); stroke-width: 2.5; stroke-linecap: round; }
/* The drum keeps whatever angle it stopped at; play state, not the animation, switches. */
.toggle-wash__drum { transform-box: view-box; transform-origin: 38px 52px; animation: toggle-wash-spin 1.1s linear infinite paused; }
.toggle-wash__machine { animation: toggle-wash-shake 0.14s linear infinite paused; }
.toggle-wash__fill { transform-box: view-box; transform-origin: 38px 72px; transform: scaleY(0); transition: transform 0.8s ease-in-out; }
.toggle-wash__wave { transform-box: view-box; transform-origin: 38px 52px; animation: toggle-wash-slosh 1.1s ease-in-out infinite paused; }
.toggle-wash__input:checked + .toggle-wash__art .toggle-wash__fill { transform: none; }
.toggle-wash__input:checked + .toggle-wash__art .toggle-wash__drum,
.toggle-wash__input:checked + .toggle-wash__art .toggle-wash__wave,
.toggle-wash__input:checked + .toggle-wash__art .toggle-wash__machine { animation-play-state: running; }
.toggle-wash__input:checked + .toggle-wash__art .toggle-wash__led { fill: light-dark(#373a40, #f1f2f3); filter: drop-shadow(0 0 2px light-dark(#373a40, #f1f2f3)); }
@media (prefers-reduced-motion: reduce) {
.toggle-wash__drum, .toggle-wash__machine, .toggle-wash__wave { animation: none; }
.toggle-wash__fill { transition: none; }
}
`;
export function WashToggle() {
const clip = `toggle-wash-${useId().replace(/[^a-zA-Z0-9]/g, '')}`;
return (
<>
<style>
{css}
</style>
<label className="toggle-wash">
<input className="toggle-wash__input" type="checkbox" role="switch" />
<svg className="toggle-wash__art" viewBox="0 0 76 88" aria-hidden="true">
<defs>
<clipPath id={clip}>
<circle cx="38" cy="52" r="19" />
</clipPath>
</defs>
<g className="toggle-wash__machine">
<rect className="toggle-wash__body" x="4" y="4" width="68" height="80" rx="6" />
<path className="toggle-wash__panel" d="M4 18V10A6 6 0 0 1 10 4H66A6 6 0 0 1 72 10V18Z" />
<rect className="toggle-wash__screen" x="11" y="8" width="22" height="6" rx="1.5" />
<circle className="toggle-wash__led" cx="44" cy="11" r="2" />
<circle className="toggle-wash__knob" cx="59" cy="11" r="4" />
<circle className="toggle-wash__ring" cx="38" cy="52" r="24" />
<circle className="toggle-wash__glass" cx="38" cy="52" r="19" />
<g clipPath={`url(#${clip})`}>
<g className="toggle-wash__fill">
<g className="toggle-wash__wave">
<rect className="toggle-wash__water" x="10" y="54" width="56" height="30" />
</g>
</g>
<g className="toggle-wash__drum">
{Array.from({ length: 12 }, (_, i) => (
<circle key={i} className="toggle-wash__hole" cx="38" cy="35" r="1" transform={`rotate(${i * 30} 38 52)`} />
))}
<path className="toggle-wash__cloth" d="M27 59Q31 54 36 58Q40 62 44 59L45 64Q38 68 29 64Z" />
<path className="toggle-wash__cloth toggle-wash__cloth--b" d="M40 38Q46 36 49 42L46 47Q42 43 38 44Z" />
<path className="toggle-wash__cloth toggle-wash__cloth--b" d="M26 44L31 41L33 48L28 50Z" />
</g>
</g>
<path className="toggle-wash__shine" d="M26 40A16 16 0 0 1 34 36" />
</g>
</svg>
<span>Wash</span>
</label>
</>
);
}Markup where the component goes, CSS in your stylesheet. No JavaScript.
<label class="toggle-wash">
<input class="toggle-wash__input" type="checkbox" role="switch"/>
<svg class="toggle-wash__art" viewBox="0 0 76 88" aria-hidden="true">
<defs>
<clipPath id="toggle-wash-clip">
<circle cx="38" cy="52" r="19"></circle>
</clipPath>
</defs>
<g class="toggle-wash__machine">
<rect class="toggle-wash__body" x="4" y="4" width="68" height="80" rx="6"></rect>
<path class="toggle-wash__panel" d="M4 18V10A6 6 0 0 1 10 4H66A6 6 0 0 1 72 10V18Z"></path>
<rect class="toggle-wash__screen" x="11" y="8" width="22" height="6" rx="1.5"></rect>
<circle class="toggle-wash__led" cx="44" cy="11" r="2"></circle>
<circle class="toggle-wash__knob" cx="59" cy="11" r="4"></circle>
<circle class="toggle-wash__ring" cx="38" cy="52" r="24"></circle>
<circle class="toggle-wash__glass" cx="38" cy="52" r="19"></circle>
<g clip-path="url(#toggle-wash-clip)">
<g class="toggle-wash__fill">
<g class="toggle-wash__wave">
<rect class="toggle-wash__water" x="10" y="54" width="56" height="30"></rect>
</g>
</g>
<g class="toggle-wash__drum">
<circle class="toggle-wash__hole" cx="38" cy="35" r="1" transform="rotate(0 38 52)"></circle>
<circle class="toggle-wash__hole" cx="38" cy="35" r="1" transform="rotate(30 38 52)"></circle>
<circle class="toggle-wash__hole" cx="38" cy="35" r="1" transform="rotate(60 38 52)"></circle>
<circle class="toggle-wash__hole" cx="38" cy="35" r="1" transform="rotate(90 38 52)"></circle>
<circle class="toggle-wash__hole" cx="38" cy="35" r="1" transform="rotate(120 38 52)"></circle>
<circle class="toggle-wash__hole" cx="38" cy="35" r="1" transform="rotate(150 38 52)"></circle>
<circle class="toggle-wash__hole" cx="38" cy="35" r="1" transform="rotate(180 38 52)"></circle>
<circle class="toggle-wash__hole" cx="38" cy="35" r="1" transform="rotate(210 38 52)"></circle>
<circle class="toggle-wash__hole" cx="38" cy="35" r="1" transform="rotate(240 38 52)"></circle>
<circle class="toggle-wash__hole" cx="38" cy="35" r="1" transform="rotate(270 38 52)"></circle>
<circle class="toggle-wash__hole" cx="38" cy="35" r="1" transform="rotate(300 38 52)"></circle>
<circle class="toggle-wash__hole" cx="38" cy="35" r="1" transform="rotate(330 38 52)"></circle>
<path class="toggle-wash__cloth" d="M27 59Q31 54 36 58Q40 62 44 59L45 64Q38 68 29 64Z"></path>
<path class="toggle-wash__cloth toggle-wash__cloth--b" d="M40 38Q46 36 49 42L46 47Q42 43 38 44Z"></path>
<path class="toggle-wash__cloth toggle-wash__cloth--b" d="M26 44L31 41L33 48L28 50Z"></path>
</g>
</g>
<path class="toggle-wash__shine" d="M26 40A16 16 0 0 1 34 36"></path>
</g>
</svg>
<span>Wash</span>
</label>@keyframes toggle-wash-spin { to { transform: rotate(360deg); } }
@keyframes toggle-wash-shake { 0%, 100% { transform: translate(0, 0); } 25% { transform: translate(0.5px, -0.3px); } 75% { transform: translate(-0.5px, 0.3px); } }
@keyframes toggle-wash-slosh { 0%, 100% { transform: rotate(-4deg); } 50% { transform: rotate(4deg); } }
.toggle-wash { display: inline-flex; align-items: center; gap: 14px; color: light-dark(#42454c, #e5e6e9); font-size: 15px; cursor: pointer; user-select: none; }
.toggle-wash__input { position: absolute; width: 1px; height: 1px; margin: 0; opacity: 0; }
.toggle-wash__art { display: block; width: 80px; height: 92px; overflow: visible; border-radius: 8px; }
.toggle-wash__input:focus-visible + .toggle-wash__art { outline: 2px solid light-dark(#5f626a, #a7abb2); outline-offset: 4px; }
.toggle-wash__body { fill: light-dark(#dbdde1, #2e3035); }
.toggle-wash__panel { fill: light-dark(#cbced3, #393c41); }
.toggle-wash__screen { fill: light-dark(#ebedf0, #232529); }
.toggle-wash__knob { fill: light-dark(#5f626a, #a7abb2); }
.toggle-wash__led { fill: light-dark(#6d7179, #7d8087); transition: fill 0.2s, filter 0.2s; }
.toggle-wash__ring { fill: light-dark(#6d7179, #7d8087); }
.toggle-wash__glass { fill: light-dark(#f3f4f7, #1a1c1f); }
.toggle-wash__water { fill: color-mix(in srgb, light-dark(#5f626a, #a7abb2) 55%, transparent); }
.toggle-wash__hole { fill: light-dark(#ebedf0, #232529); }
.toggle-wash__cloth { fill: light-dark(#50535b, #c6c8cd); }
.toggle-wash__cloth--b { fill: light-dark(#6d7179, #7d8087); }
.toggle-wash__shine { fill: none; stroke: color-mix(in srgb, light-dark(#373a40, #f1f2f3) 22%, transparent); stroke-width: 2.5; stroke-linecap: round; }
/* The drum keeps whatever angle it stopped at; play state, not the animation, switches. */
.toggle-wash__drum { transform-box: view-box; transform-origin: 38px 52px; animation: toggle-wash-spin 1.1s linear infinite paused; }
.toggle-wash__machine { animation: toggle-wash-shake 0.14s linear infinite paused; }
.toggle-wash__fill { transform-box: view-box; transform-origin: 38px 72px; transform: scaleY(0); transition: transform 0.8s ease-in-out; }
.toggle-wash__wave { transform-box: view-box; transform-origin: 38px 52px; animation: toggle-wash-slosh 1.1s ease-in-out infinite paused; }
.toggle-wash__input:checked + .toggle-wash__art .toggle-wash__fill { transform: none; }
.toggle-wash__input:checked + .toggle-wash__art .toggle-wash__drum,
.toggle-wash__input:checked + .toggle-wash__art .toggle-wash__wave,
.toggle-wash__input:checked + .toggle-wash__art .toggle-wash__machine { animation-play-state: running; }
.toggle-wash__input:checked + .toggle-wash__art .toggle-wash__led { fill: light-dark(#373a40, #f1f2f3); filter: drop-shadow(0 0 2px light-dark(#373a40, #f1f2f3)); }
@media (prefers-reduced-motion: reduce) {
.toggle-wash__drum, .toggle-wash__machine, .toggle-wash__wave { animation: none; }
.toggle-wash__fill { transition: none; }
}Goal: Create a "Wash" toggle switch.
Visual behavior: A wash switch drawn as a washing machine: water fills the drum, the clothes tumble round and the machine shakes; off stops the drum where it is and drains it.
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 drum spin, the slosh and the shake loop all the time and animation-play-state switches them, so the drum keeps whatever angle it stopped at.
- The water is clipped to the porthole and scales up from the bottom as it fills. 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 drum fills without spinning.
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 "Wash" toggle switch.
Visual behavior: A wash switch drawn as a washing machine: water fills the drum, the clothes tumble round and the machine shakes; off stops the drum where it is and drains it.
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 drum spin, the slosh and the shake loop all the time and animation-play-state switches them, so the drum keeps whatever angle it stopped at.
- The water is clipped to the porthole and scales up from the bottom as it fills. 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 drum fills without spinning.
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 wash switch drawn as a washing machine: water fills the drum, the clothes tumble round and the machine shakes; off stops the drum where it is and drains it.
- Dependencies
- None
- Difficulty
- Intermediate
- Triggers
- click, state change, loop
- Techniques
- keyframes, clip path, 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 drum fills without spinning.