Wash

No dependencies
Code format
React + Tailwind
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>
    </>
  );
}

Free to use in any project, commercial included. MIT-0, no attribution required. Licence

Install with shadcn

npx shadcn@latest add @movin/toggle-wash
Source on GitHub

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.

More toggles

See all
Broadcast
Door
Earbuds
Flag