Sail
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>
</>
);
}Markup where the component goes, CSS in your stylesheet. No JavaScript.
<label class="toggle-sail">
<input class="toggle-sail__input" type="checkbox" role="switch"/>
<svg class="toggle-sail__art" viewBox="0 0 100 84" aria-hidden="true">
<g class="toggle-sail__rock">
<g class="toggle-sail__lean">
<line class="toggle-sail__mast" x1="50" y1="10" x2="50" y2="64"></line>
<g class="toggle-sail__fore">
<path class="toggle-sail__jib toggle-sail__flat" d="M48.5 16V57H29Z"></path>
<path class="toggle-sail__jib toggle-sail__full" d="M48.5 16Q36 34 27 57H48.5Z"></path>
</g>
<g class="toggle-sail__main">
<path class="toggle-sail__cloth toggle-sail__flat" d="M51.5 12V57H78Z"></path>
<path class="toggle-sail__cloth toggle-sail__full" d="M51.5 12Q75 28 80 57H51.5Z"></path>
<path class="toggle-sail__seam" d="M51.5 27H62M51.5 42H70"></path>
</g>
<line class="toggle-sail__boom" x1="50" y1="58" x2="79" y2="58"></line>
<path class="toggle-sail__hull" d="M16 62H84L76 73Q74 75 70 75H30Q26 75 24 73Z"></path>
</g>
</g>
<path class="toggle-sail__sea" d="M0 70Q8 67 16 70T32 70T48 70T64 70T80 70T96 70T112 70V84H0Z"></path>
<path class="toggle-sail__wave" d="M10 78q4-2 8 0M58 80q4-2 8 0M78 76q4-2 8 0"></path>
</svg>
<span>Sail</span>
</label>@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; }
}Goal: Create a "Sail" toggle switch.
Visual behavior: 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.
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.
- Each sail scales up from its boom, and a flat sail crossfades into a full one as the wind fills it.
- The boat leans with the checked state and a slow keyframe rocks it once the sails are up. 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 sails go up at once and the boat does not rock.
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 "Sail" toggle switch.
Visual behavior: 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.
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.
- Each sail scales up from its boom, and a flat sail crossfades into a full one as the wind fills it.
- The boat leans with the checked state and a slow keyframe rocks it once the sails are up. 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 sails go up at once and the boat does not rock.
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 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.