Rake
import { useState } from 'react';
const css = `
@keyframes cb-rake-tool { 0% { offset-distance: 0%; opacity: 0; } 10% { opacity: 1; } 85% { offset-distance: 100%; opacity: 1; } 100% { offset-distance: 100%; opacity: 0; } }
.cb-rake { display: inline-flex; align-items: center; gap: 14px; color: light-dark(#42454c, #e5e6e9); font-size: 15px; cursor: pointer; user-select: none; }
.cb-rake__input { position: absolute; width: 1px; height: 1px; margin: 0; opacity: 0; }
.cb-rake__art { position: relative; display: block; width: 92px; height: 60px; border-radius: 8px; }
.cb-rake__input:focus-visible + .cb-rake__art { outline: 2px solid light-dark(#5f626a, #a7abb2); outline-offset: 4px; }
.cb-rake__tray { position: absolute; inset: 0; width: 92px; height: 60px; }
.cb-rake__frame { fill: light-dark(#cbced3, #393c41); }
.cb-rake__sand { fill: light-dark(#dbdde1, #2e3035); }
.cb-rake__furrow { fill: none; stroke: light-dark(#cbced3, #393c41); stroke-width: 1.1; stroke-linecap: round; }
.cb-rake__stone { fill: light-dark(#6d7179, #7d8087); }
/* The groove is three strokes laid on the same path: a wide shadow, the sand in between and the tines' lines. */
.cb-rake__groove { fill: none; stroke-linecap: round; stroke-linejoin: round; stroke-dasharray: 1; stroke-dashoffset: 1; transition: stroke-dashoffset 0.35s ease-in; }
.cb-rake__groove--edge { stroke: light-dark(#6d7179, #7d8087); stroke-width: 9; }
.cb-rake__groove--bed { stroke: light-dark(#dbdde1, #2e3035); stroke-width: 6.5; }
.cb-rake__groove--line { stroke: light-dark(#6d7179, #7d8087); stroke-width: 1.2; }
.cb-rake__input:checked + .cb-rake__art .cb-rake__groove { stroke-dashoffset: 0; transition: stroke-dashoffset 0.9s cubic-bezier(0.45, 0, 0.55, 1); }
.cb-rake__tool { position: absolute; left: 0; top: 0; width: 14px; height: 4px; opacity: 0; offset-path: path('M20 30L36 44L70 16'); offset-rotate: auto 90deg; }
.cb-rake__tool::before { content: ''; position: absolute; inset: 0; border-radius: 1.5px; background: light-dark(#50535b, #c6c8cd); box-shadow: 0 1px 0 light-dark(#6d7179, #7d8087); }
.cb-rake__tool::after { content: ''; position: absolute; left: 6px; bottom: 3px; width: 2px; height: 22px; border-radius: 1px; background: light-dark(#5f626a, #a7abb2); }
.cb-rake[data-armed] .cb-rake__input:checked + .cb-rake__art .cb-rake__tool { animation: cb-rake-tool 1.05s cubic-bezier(0.45, 0, 0.55, 1); }
@media (prefers-reduced-motion: reduce) {
.cb-rake__groove, .cb-rake__input:checked + .cb-rake__art .cb-rake__groove { transition: none; }
.cb-rake[data-armed] .cb-rake__input:checked + .cb-rake__art .cb-rake__tool { animation: none; }
}
`;
const TICK = 'M20 30L36 44L70 16';
export function RakeCheckbox() {
// The rake only appears once someone ticks it.
const [armed, setArmed] = useState(false);
return (
<>
<style>
{css}
</style>
<label className="cb-rake" data-armed={armed || undefined}>
<input className="cb-rake__input" type="checkbox" onChange={() => setArmed(true)} />
<span className="cb-rake__art" aria-hidden="true">
<svg className="cb-rake__tray" viewBox="0 0 92 60">
<rect className="cb-rake__frame" x="0" y="0" width="92" height="60" rx="8" />
<rect className="cb-rake__sand" x="4" y="4" width="84" height="52" rx="5" />
{[12, 20, 28, 36, 44, 52].map((y) => (
<path key={y} className="cb-rake__furrow" d={`M8 ${y}Q30 ${y - 2} 46 ${y}T84 ${y}`} />
))}
<path className="cb-rake__groove cb-rake__groove--edge" pathLength={1} d={TICK} />
<path className="cb-rake__groove cb-rake__groove--bed" pathLength={1} d={TICK} />
<path className="cb-rake__groove cb-rake__groove--line" pathLength={1} d={TICK} />
<ellipse className="cb-rake__stone" cx="78" cy="46" rx="5" ry="3.5" />
</svg>
<span className="cb-rake__tool" />
</span>
<span>Rest day</span>
</label>
</>
);
}Markup where the component goes, CSS in your stylesheet, script before </body>. In Vue, Svelte or Angular, call initCbRake(el) on the mounted element instead.
<label class="cb-rake">
<input class="cb-rake__input" type="checkbox"/>
<span class="cb-rake__art" aria-hidden="true">
<svg class="cb-rake__tray" viewBox="0 0 92 60">
<rect class="cb-rake__frame" x="0" y="0" width="92" height="60" rx="8"></rect>
<rect class="cb-rake__sand" x="4" y="4" width="84" height="52" rx="5"></rect>
<path class="cb-rake__furrow" d="M8 12Q30 10 46 12T84 12"></path>
<path class="cb-rake__furrow" d="M8 20Q30 18 46 20T84 20"></path>
<path class="cb-rake__furrow" d="M8 28Q30 26 46 28T84 28"></path>
<path class="cb-rake__furrow" d="M8 36Q30 34 46 36T84 36"></path>
<path class="cb-rake__furrow" d="M8 44Q30 42 46 44T84 44"></path>
<path class="cb-rake__furrow" d="M8 52Q30 50 46 52T84 52"></path>
<path class="cb-rake__groove cb-rake__groove--edge" pathLength="1" d="M20 30L36 44L70 16"></path>
<path class="cb-rake__groove cb-rake__groove--bed" pathLength="1" d="M20 30L36 44L70 16"></path>
<path class="cb-rake__groove cb-rake__groove--line" pathLength="1" d="M20 30L36 44L70 16"></path>
<ellipse class="cb-rake__stone" cx="78" cy="46" rx="5" ry="3.5"></ellipse>
</svg>
<span class="cb-rake__tool"></span>
</span>
<span>Rest day</span>
</label>@keyframes cb-rake-tool { 0% { offset-distance: 0%; opacity: 0; } 10% { opacity: 1; } 85% { offset-distance: 100%; opacity: 1; } 100% { offset-distance: 100%; opacity: 0; } }
.cb-rake { display: inline-flex; align-items: center; gap: 14px; color: light-dark(#42454c, #e5e6e9); font-size: 15px; cursor: pointer; user-select: none; }
.cb-rake__input { position: absolute; width: 1px; height: 1px; margin: 0; opacity: 0; }
.cb-rake__art { position: relative; display: block; width: 92px; height: 60px; border-radius: 8px; }
.cb-rake__input:focus-visible + .cb-rake__art { outline: 2px solid light-dark(#5f626a, #a7abb2); outline-offset: 4px; }
.cb-rake__tray { position: absolute; inset: 0; width: 92px; height: 60px; }
.cb-rake__frame { fill: light-dark(#cbced3, #393c41); }
.cb-rake__sand { fill: light-dark(#dbdde1, #2e3035); }
.cb-rake__furrow { fill: none; stroke: light-dark(#cbced3, #393c41); stroke-width: 1.1; stroke-linecap: round; }
.cb-rake__stone { fill: light-dark(#6d7179, #7d8087); }
/* The groove is three strokes laid on the same path: a wide shadow, the sand in between and the tines' lines. */
.cb-rake__groove { fill: none; stroke-linecap: round; stroke-linejoin: round; stroke-dasharray: 1; stroke-dashoffset: 1; transition: stroke-dashoffset 0.35s ease-in; }
.cb-rake__groove--edge { stroke: light-dark(#6d7179, #7d8087); stroke-width: 9; }
.cb-rake__groove--bed { stroke: light-dark(#dbdde1, #2e3035); stroke-width: 6.5; }
.cb-rake__groove--line { stroke: light-dark(#6d7179, #7d8087); stroke-width: 1.2; }
.cb-rake__input:checked + .cb-rake__art .cb-rake__groove { stroke-dashoffset: 0; transition: stroke-dashoffset 0.9s cubic-bezier(0.45, 0, 0.55, 1); }
.cb-rake__tool { position: absolute; left: 0; top: 0; width: 14px; height: 4px; opacity: 0; offset-path: path('M20 30L36 44L70 16'); offset-rotate: auto 90deg; }
.cb-rake__tool::before { content: ''; position: absolute; inset: 0; border-radius: 1.5px; background: light-dark(#50535b, #c6c8cd); box-shadow: 0 1px 0 light-dark(#6d7179, #7d8087); }
.cb-rake__tool::after { content: ''; position: absolute; left: 6px; bottom: 3px; width: 2px; height: 22px; border-radius: 1px; background: light-dark(#5f626a, #a7abb2); }
.cb-rake[data-armed] .cb-rake__input:checked + .cb-rake__art .cb-rake__tool { animation: cb-rake-tool 1.05s cubic-bezier(0.45, 0, 0.55, 1); }
@media (prefers-reduced-motion: reduce) {
.cb-rake__groove, .cb-rake__input:checked + .cb-rake__art .cb-rake__groove { transition: none; }
.cb-rake[data-armed] .cb-rake__input:checked + .cb-rake__art .cb-rake__tool { animation: none; }
}function initCbRake(root) {
// The motion keyframes only run once the control has been used, never on page load.
function onChange() {
root.setAttribute('data-armed', '');
}
root.addEventListener('change', onChange);
return function () {
root.removeEventListener('change', onChange);
};
}
document.querySelectorAll('.cb-rake').forEach(initCbRake);Goal: Create a "Rake" checkbox.
Visual behavior: A rest day checkbox drawn as a zen garden: a rake draws the tick through the raked sand as a groove; unchecking smooths it back into the lines.
Technique:
- A native checkbox, visually hidden but focusable, sits inside a <label> before an aria-hidden drawing and the visible text; the checked state is styled from :checked through the adjacent sibling combinator.
- The groove is three strokes on the same path, a wide shadow, the sand between and the tine lines, drawn in by their dash offsets.
- The rake rides the path on offset-path with offset-rotate, so its head turns with the stroke. Keyframes that must not play on page load hang off a data-armed attribute set on the first change.
Accessibility: A native checkbox inside a <label>, so Space toggles it and the visible label names it; :focus-visible on the input outlines the drawing, which is aria-hidden. Under prefers-reduced-motion the groove appears at once with no rake.
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 "Rake" checkbox.
Visual behavior: A rest day checkbox drawn as a zen garden: a rake draws the tick through the raked sand as a groove; unchecking smooths it back into the lines.
Technique:
- A native checkbox, visually hidden but focusable, sits inside a <label> before an aria-hidden drawing and the visible text; the checked state is styled from :checked through the adjacent sibling combinator.
- The groove is three strokes on the same path, a wide shadow, the sand between and the tine lines, drawn in by their dash offsets.
- The rake rides the path on offset-path with offset-rotate, so its head turns with the stroke. Keyframes that must not play on page load hang off a data-armed attribute set on the first change.
Accessibility: A native checkbox inside a <label>, so Space toggles it and the visible label names it; :focus-visible on the input outlines the drawing, which is aria-hidden. Under prefers-reduced-motion the groove appears at once with no rake.
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 rest day checkbox drawn as a zen garden: a rake draws the tick through the raked sand as a groove; unchecking smooths it back into the lines.
- Dependencies
- None
- Difficulty
- Intermediate
- Triggers
- click, state change, draw
- Techniques
- stroke dashoffset, keyframes, svg
- Reduced motion
- Respected
- Theme
- Light and dark (follows your page's color-scheme)
A native checkbox inside a <label>, so Space toggles it and the visible label names it; :focus-visible on the input outlines the drawing, which is aria-hidden. Under prefers-reduced-motion the groove appears at once with no rake.