Sail

No dependencies
Code format
React + Tailwind
const css = `
  @keyframes toggle-sail-rock { 0%, 100% { transform: rotate(-2deg) translateY(0); } 50% { transform: rotate(2deg) translateY(-1px); } }
  .toggle-sail { display: inline-flex; align-items: center; gap: 14px; color: light-dark(#42454c, #e5e6e9); font-size: 15px; cursor: pointer; user-select: none; }
  .toggle-sail__input { position: absolute; width: 1px; height: 1px; margin: 0; opacity: 0; }
  .toggle-sail__art { display: block; width: 104px; height: 88px; overflow: visible; border-radius: 8px; }
  .toggle-sail__input:focus-visible + .toggle-sail__art { outline: 2px solid light-dark(#5f626a, #a7abb2); outline-offset: 4px; }
  .toggle-sail__sea { fill: light-dark(#ebedf0, #232529); }
  .toggle-sail__wave { fill: none; stroke: light-dark(#dbdde1, #2e3035); stroke-width: 1.2; stroke-linecap: round; }
  .toggle-sail__hull { fill: light-dark(#cbced3, #393c41); }
  .toggle-sail__mast { stroke: light-dark(#5f626a, #a7abb2); stroke-width: 2; stroke-linecap: round; }
  .toggle-sail__boom { stroke: light-dark(#6d7179, #7d8087); stroke-width: 2; stroke-linecap: round; }
  .toggle-sail__cloth { fill: light-dark(#42454c, #e5e6e9); }
  .toggle-sail__jib { fill: light-dark(#50535b, #c6c8cd); }
  .toggle-sail__seam { fill: none; stroke: light-dark(#5f626a, #a7abb2); stroke-width: 0.8; }
  /* Furled, the sails are a bundle along the boom; hoisting runs them up the mast, then they fill. */
  .toggle-sail__main { transform-box: view-box; transform-origin: 51px 57px; transform: scaleY(0.07); transition: transform 0.7s cubic-bezier(0.5, 0, 0.3, 1); }
  .toggle-sail__fore { transform-box: view-box; transform-origin: 49px 57px; transform: scaleY(0.07); transition: transform 0.6s cubic-bezier(0.5, 0, 0.3, 1); }
  .toggle-sail__flat { transition: opacity 0.4s; }
  .toggle-sail__full { opacity: 0; transition: opacity 0.4s; }
  .toggle-sail__lean { transform-box: view-box; transform-origin: 50px 72px; transition: transform 1s ease-in-out; }
  .toggle-sail__input:checked + .toggle-sail__art .toggle-sail__main { transform: none; }
  .toggle-sail__input:checked + .toggle-sail__art .toggle-sail__fore { transform: none; transition-delay: 0.3s; }
  .toggle-sail__input:checked + .toggle-sail__art .toggle-sail__flat { opacity: 0; transition-delay: 0.8s; }
  .toggle-sail__input:checked + .toggle-sail__art .toggle-sail__full { opacity: 1; transition-delay: 0.8s; }
  .toggle-sail__input:checked + .toggle-sail__art .toggle-sail__lean { transform: rotate(-6deg); transition-delay: 0.9s; }
  .toggle-sail__input:checked + .toggle-sail__art .toggle-sail__rock { animation: toggle-sail-rock 2.4s ease-in-out 1.6s infinite; }
  .toggle-sail__rock { transform-box: view-box; transform-origin: 50px 72px; }
  @media (prefers-reduced-motion: reduce) {
    .toggle-sail__main, .toggle-sail__fore, .toggle-sail__flat, .toggle-sail__full, .toggle-sail__lean { transition: none; }
    .toggle-sail__input:checked + .toggle-sail__art .toggle-sail__rock { animation: none; }
  }
`;

export function SailToggle() {
  return (
    <>
      <style>
        {css}
      </style>
      <label className="toggle-sail">
        <input className="toggle-sail__input" type="checkbox" role="switch" />
        <svg className="toggle-sail__art" viewBox="0 0 100 84" aria-hidden="true">
          <g className="toggle-sail__rock">
            <g className="toggle-sail__lean">
              <line className="toggle-sail__mast" x1="50" y1="10" x2="50" y2="64" />
              <g className="toggle-sail__fore">
                <path className="toggle-sail__jib toggle-sail__flat" d="M48.5 16V57H29Z" />
                <path className="toggle-sail__jib toggle-sail__full" d="M48.5 16Q36 34 27 57H48.5Z" />
              </g>
              <g className="toggle-sail__main">
                <path className="toggle-sail__cloth toggle-sail__flat" d="M51.5 12V57H78Z" />
                <path className="toggle-sail__cloth toggle-sail__full" d="M51.5 12Q75 28 80 57H51.5Z" />
                <path className="toggle-sail__seam" d="M51.5 27H62M51.5 42H70" />
              </g>
              <line className="toggle-sail__boom" x1="50" y1="58" x2="79" y2="58" />
              <path className="toggle-sail__hull" d="M16 62H84L76 73Q74 75 70 75H30Q26 75 24 73Z" />
            </g>
          </g>
          <path className="toggle-sail__sea" d="M0 70Q8 67 16 70T32 70T48 70T64 70T80 70T96 70T112 70V84H0Z" />
          <path className="toggle-sail__wave" d="M10 78q4-2 8 0M58 80q4-2 8 0M78 76q4-2 8 0" />
        </svg>
        <span>Sail</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-sail
Source on GitHub

About this component

A sail switch drawn as a boat: the mainsail and jib run up the mast and fill with wind, and the boat heels over and rocks on the waves; off lowers them back to the boom.

Dependencies
None
Difficulty
Intermediate
Triggers
click, state change, loop
Techniques
keyframes, css transition, 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 sails go up at once and the boat does not rock.

More toggles

See all
Shades
Shell
Sign
Signal