Rake

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

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

Install with shadcn

npx shadcn@latest add @movin/checkbox-rake
Source on GitHub

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.

More checkboxes

See all
Skate
Spray
Strap
Stud