# Check Draw (Moonarc)

A checkbox whose tick is drawn. A label wraps a native input with appearance: none and an SVG beside it: the box fills on the fast token, then the tick (a path with pathLength 1) runs its dash offset from 1 to 0 on the preset curve. The indeterminate state draws a bar instead. The value submits, required and disabled work, the keyboard is the browser's. Zero JavaScript.

- Import: `import CheckDraw from '@moonarc/core/CheckDraw'`
- Install: `npx astro add moonarc` · copy-paste: `npx shadcn@latest add https://moonarc.dev/r/check-draw.json`
- Tier A · category ui · trigger click
- Readout: `<CheckDraw name="terms" label="I agree" required>`
- Browser support: widely (every browser)
- Measured cost: 0 B JS (CSS 6.6 kB raw)
- Page: https://moonarc.dev/components/check-draw/

## Props

| Prop | Type | Default | Description |
|---|---|---|---|
| `name` | `string` | none | Form field name. |
| `value` | `string` | `'on'` | Submitted value. |
| `checked` | `boolean` | `false` | Initial state. |
| `label` | `string` | none | Visible label; the whole label toggles the box. Without it, pass aria-label. |
| `size` | `string` | `'1.25rem'` | Box size, any CSS length; the stroke scales with it. |
| `color` | `string` | none | Box colour when checked; default the ink token. |
| `--ma-checkdraw-edge` | `CSS colour (custom property)` | `the ink token at 45 %` | The unchecked box's border. Set it in a rule or style on the component or any ancestor that knows its ground. Use a border measured at 3 : 1 against it (WCAG 1.4.11) where 45 % of the ink falls short, for example on a coloured panel. |

## Usage

```astro
<CheckDraw name="terms" label="I agree to the terms" required />
<CheckDraw name="news" label="Send me the newsletter" checked />

<!-- indeterminate is a DOM property, set from your script -->
<CheckDraw id="all" name="all" label="Select all" />
<script>document.getElementById('all').indeterminate = true</script>
```

## Reduced motion

The box fills and the tick appears at once.

## With ClientRouter

A native input; its state travels with the page. Nothing to bind.

## Craft

- The input stays a checkbox in every sense (FormData, required, disabled, the label click, Space, the accessibility tree), and appearance: none only removes the native drawing. The SVG is a sibling that reads :checked and :indeterminate through the general sibling combinator; no :has() needed.
- pathLength="1" makes the dash arithmetic unit-free: dasharray 1, dashoffset 1 is a hidden stroke, 0 is the whole tick, whatever the box size. The tick is drawn on the preset curve after a delay of half the fast token, so the box has filled and the stroke lands on ink.
- Unchecking runs the offset back on the exit curve in the fast duration: the tick leaves the way it came, quickly, and the box empties with it.
- indeterminate is a DOM property, not an attribute, so the bar has no server state; the stage sets it from its script the way a "select all" control would.
- forced-colors rules keep the box visible under Windows high contrast: a ButtonText border, Highlight when checked, the stroke in HighlightText.

## Replaces

- Checkbox (Motion Primitives)
- Checkbox (shadcn / Radix)
- AnimatedCheckbox (Aceternity)

## Source

```astro
---
/**
 * CheckDraw — a checkbox whose tick is drawn, zero JS. A <label> wraps a
 * native `<input type="checkbox">` (`appearance: none`, still focusable,
 * still submitted) and an SVG beside it: the tick is a path with
 * `pathLength="1"` whose dash offset runs 1 → 0 on the preset curve when
 * the input is `:checked`; `:indeterminate` (a DOM property your script
 * sets) draws a short bar instead. The box fills on the fast token first,
 * so the stroke lands on ink. Radios are not this component.
 */
import type { HTMLAttributes } from 'astro/types';

interface Props extends Omit<HTMLAttributes<'input'>, 'type' | 'size'> {
  name?: string;
  value?: string;
  checked?: boolean;
  /** Visible label. Without it, pass aria-label. */
  label?: string;
  /** Box size, any CSS length. */
  size?: string;
  /** Box colour when checked; default the ink token. */
  color?: string;
}

const { name, value = 'on', checked = false, label, size = '1.25rem', color, class: className, style, ...rest } = Astro.props;
const vars = [`--ma-checkdraw-size:${size}`, color && `--ma-checkdraw-on:${color}`, typeof style === 'string' ? style : ''].filter(Boolean).join(';');
---

<label class:list={['ma-checkdraw', className]} style={vars}>
  <span class="ma-checkdraw__box">
    <input type="checkbox" class="ma-checkdraw__input" name={name} value={value} checked={checked} {...rest} />
    <svg class="ma-checkdraw__mark" viewBox="0 0 24 24" aria-hidden="true">
      <path class="ma-checkdraw__tick" d="M5 12.5l4.5 4.5L19 7" pathLength="1" />
      <path class="ma-checkdraw__bar" d="M6 12h12" pathLength="1" />
    </svg>
  </span>
  {label && <span class="ma-checkdraw__label">{label}</span>}
</label>

<style is:global>
  @layer components {
    :where(.ma-checkdraw) {
      display: inline-flex;
      align-items: center;
      gap: 0.6em;
      cursor: pointer;
      -webkit-tap-highlight-color: transparent;
    }
    :where(.ma-checkdraw__box) {
      position: relative;
      flex: none;
      display: grid;
      inline-size: var(--ma-checkdraw-size, 1.25rem);
      block-size: var(--ma-checkdraw-size, 1.25rem);
    }
    :where(.ma-checkdraw__input) {
      /* the box: the input itself, so focus, the value and the keyboard are native */
      appearance: none;
      -webkit-appearance: none;
      position: relative;
      grid-area: 1 / 1;
      inline-size: 100%;
      block-size: 100%;
      margin: 0;
      /* --ma-checkdraw-edge: a section that knows its ground sets a border measured on it (HeroWaitlist, 7G) */
      border: 1.5px solid var(--ma-checkdraw-edge, color-mix(in srgb, var(--ma-ink) 45%, transparent));
      border-radius: 0.3em;
      background: transparent;
      cursor: pointer;
      font: inherit;
      transition:
        background-color var(--ma-duration-fast) var(--ma-ease-out),
        border-color var(--ma-duration-fast) var(--ma-ease-out);
    }
    :where(.ma-checkdraw__input)::after {
      content: '';
      position: absolute;
      inset: min(0px, calc((var(--ma-checkdraw-size, 1.25rem) - 26px) / 2));
    }
    :where(.ma-checkdraw__input:checked),
    :where(.ma-checkdraw__input:indeterminate) {
      background: var(--ma-checkdraw-on, var(--ma-ink));
      border-color: var(--ma-checkdraw-on, var(--ma-ink));
    }
    :where(.ma-checkdraw__mark) {
      grid-area: 1 / 1;
      inline-size: 100%;
      block-size: 100%;
      padding: 12%;
      pointer-events: none;
      fill: none;
      stroke: var(--ma-panel);
      stroke-width: 3;
      stroke-linecap: round;
      stroke-linejoin: round;
    }
    :where(.ma-checkdraw__tick),
    :where(.ma-checkdraw__bar) {
      /* pathLength 1: the dash is the whole stroke, offset 1 hides it, 0 shows it */
      stroke-dasharray: 1;
      stroke-dashoffset: 1;
      transition: stroke-dashoffset var(--ma-duration-fast) var(--ma-ease-in);
    }
    :where(.ma-checkdraw__input:checked ~ .ma-checkdraw__mark .ma-checkdraw__tick),
    :where(.ma-checkdraw__input:indeterminate ~ .ma-checkdraw__mark .ma-checkdraw__bar) {
      stroke-dashoffset: 0;
      transition: stroke-dashoffset var(--ma-duration) var(--ma-ease);
      /* the stroke waits for the box to fill, so it is drawn on ink, not on nothing */
      transition-delay: calc(var(--ma-duration-fast) / 2);
    }
    :where(.ma-checkdraw__input:focus-visible) {
      outline: 2px solid var(--ma-checkdraw-on, var(--ma-ink));
      outline-offset: 3px;
    }
    :where(.ma-checkdraw__input:disabled) {
      cursor: not-allowed;
      opacity: 0.45;
      transition: none;
    }
    :where(.ma-checkdraw__input:disabled ~ .ma-checkdraw__mark *) {
      transition: none;
    }
    :where(.ma-checkdraw:has(.ma-checkdraw__input:disabled)) {
      cursor: not-allowed;
    }
    :where(.ma-checkdraw:has(.ma-checkdraw__input:disabled) .ma-checkdraw__label) {
      opacity: 0.6;
    }
    :where(.ma-checkdraw__label) {
      line-height: 1.2;
      user-select: none;
    }
    @media (forced-colors: active) {
      :where(.ma-checkdraw__input) {
        border-color: ButtonText;
        background: Canvas;
      }
      :where(.ma-checkdraw__input:checked),
      :where(.ma-checkdraw__input:indeterminate) {
        background: Highlight;
        border-color: Highlight;
      }
      :where(.ma-checkdraw__mark) {
        stroke: HighlightText;
      }
      :where(.ma-checkdraw__input:disabled) {
        border-color: GrayText;
        opacity: 1;
      }
    }
    @media (prefers-reduced-motion: reduce) {
      :where(.ma-checkdraw__input),
      :where(.ma-checkdraw__tick),
      :where(.ma-checkdraw__bar) {
        transition: none;
      }
    }
  }
</style>

```
