Susegad UI
Register
Theme
Palette

Components

Checkbox

A native checkbox that submits, validates and takes Space with or without JavaScript.

quiet warm playful

Open the live demo npx susegad add check

Stands on: Core, Core: components, Engine, Stamp, Tokens

The prompt

the prompt

A box to tick, the oldest mark on a form.

A native checkbox that works with no JavaScript at all: it submits, it validates, Space ticks it, and a screen reader says what it is. The register changes how the box and the tick are drawn, never how the control behaves.

<link rel="stylesheet" href="susegad/components/check/check.css">
<script type="module" src="susegad/components/check/check.js"></script>

<sg-check>
  <label><input type="checkbox" name="rules" value="agreed" required> I have read the house rules</label>
</sg-check>

<sg-check controls="x-breakfast x-pickup">
  <label><input type="checkbox" id="x-all"> All extras</label>
</sg-check>
AttributeValuesWhat it does
indeterminatepresent or absentShows "some" (a dash). HTML has no attribute for this, so the element sets the input's indeterminate property. A click clears it.
controlsids of child checkboxesMakes this a "select all" box: checked when all are, mixed when some are, clear when none are. Ticking it ticks them all.
seedany wordThe hand-drawn marks. Defaults to the label.

The prompt

Make a checkbox web component that wraps a real <input type="checkbox"> inside its <label>, so the form works with JavaScript turned off. Style the native input with appearance: none as a hairline box with the tick drawn in CSS: a ::before clipped to a tick shape with clip-path, scaled in when checked, clipped to a bar when indeterminate, all in token colours with system-colour fallbacks. Put the box in the first column of a grid and align it with the first line of the label using the lh unit, so long labels and Devanagari or Kannada labels line up. Give it three registers. Quiet: exactly that CSS. Warm: lay an aria-hidden SVG over the box, in the same grid cell, with a box sketched in four separate pencil strokes that each run a little past their corners, so the corners cross and the box reads as drawn even at rest, and a heavy pencil tick half as thick again as the CSS one: resample each stroke and push it sideways with seeded noise so it wavers like a hand, and give the tick the small flick a pencil makes at the end. When someone ticks it, draw the tick in with a stroke-dashoffset animation over about a quarter of a second, starting slow the way a pen sets down; never animate on first paint. Playful: make the box a little rubber-stamp block, a heavy inked edge with a fine inner rule, and stamp a bold filled tick in ink, cut bigger than the box so it overprints the edge, with a seeded tilt, three spots of ink, and the Wave 1 stamp's ink texture starving it in places; press it down past flat (0.92) and back. Support a "select all" box through a controls attribute: it reflects its children as checked, mixed or clear, and ticking it ticks every child and fires real change events. In forced-colours mode, bring back the native checkbox and hide the drawing. Under reduced motion, marks appear with no movement.

Words to code

When you sayTechniqueWhat happens
a real <input type="checkbox"> inside its <label>native firstThe input submits, validates (required), takes Space and names itself from the label. With JavaScript off the element is never defined and none of that changes.
a ::before clipped to a tick shapeCSS shapesOne pseudo-element, clip-path: polygon(...) for the tick and inset(...) for the dash, scaled from 0 to 1 on :checked and :indeterminate. The colour is --sg-accent-text with a CanvasText fallback.
align it with the first line using the lh unitlayoutmargin-top: calc((1lh - 1.15em) / 2) centres the box on the first line whatever the script's line height. The browser check measures it within 3px for Marathi, Kannada and Konkani labels.
in the same grid celllayeringThe input and the SVG both take grid-area: 1 / 1; the SVG has pointer-events: none, so every click still lands on the real input.
wavers like a hand … the flick a pencil makesnoisehandLine resamples the stroke every 0.8 units and moves each point along its normal by seeded Perlin noise. The tick's last point runs 8% past the corner.
a box sketched in four strokes … the corners crosssketchsketchBox(seed) jitters the four corners, then draws each side from a seeded 0.35 to 1 times 1.8 units before its corner to as far past the next, each with handLine. Four M commands in one path.
a heavy pencil tickstrokestroke-width: 3.45 on the 20-unit grid, 1.5 times the old 2.3, so the mark reads at 1x.
over about a quarter of a secondWeb Animationsarrival('ambient', 'draw') is 280 ms on cubic-bezier(0.55, 0.05, 0.3, 1), so the pen visibly starts and runs; the finished animation is cancelled.
a rubber-stamp block … overprints the edgestampThe playful box is pencilBox at a 2.1 stroke plus a 0.7 inner rule at 70%, hidden once the stamp prints. The tick is STAMP_TICK scaled 1.4 about a point just up and right of centre, so its long arm crosses the top edge.
the Wave 1 stamp's ink texturealpha maskinkMask() from stamp.core.js paints a 56 × 56 alpha mask (2 per unit) into a canvas, once per label, and an SVG <mask style="mask-type: alpha"> prints the tick through it. A tick this small can lose too much to one soft patch, so the skin takes the first of six seed variants that still prints 85% of the middle.
draw the tick in … never animate on first painta real changeThe skin remembers the last state and animates stroke-dashoffset only on a change from clear to ticked.
a "select all" boxtri-statetriState(children) gives checked, mixed or clear. The parent sets aria-controls, and a screen reader hears "mixed". Ticking it sets each child and dispatches input and change, so the children's own drawings and any listeners follow.
bring back the native checkboxforced coloursUnder forced-colors: active the input gets appearance: auto and the drawing display: none, so high-contrast users get the system control in system colours.

Accessibility

  • The native checkbox is the control: its name comes from the label, its state from the browser, "mixed" included.
  • Every drawing is aria-hidden and never takes a click.
  • The box edge, the pencil and the tick are 3:1 or more against every surface in every palette and theme (tested).
  • A required box left clear is marked by the browser; after interaction the edge turns --sg-danger too (:user-invalid), never colour alone, because the browser's message says what is wrong.
  • Forced colours: the system checkbox. Reduced motion: no movement.

<sg-check> wraps one native <input type="checkbox"> and its label. Everything a checkbox does, the input does, with or without JavaScript.

Use

<link rel="stylesheet" href="susegad/components/check/check.css">
<script type="module" src="susegad/components/check/check.js"></script>

<sg-check>
  <label><input type="checkbox" name="extras" value="breakfast" checked> Breakfast on the balcão</label>
</sg-check>

A label beside the input (<input id="doc-x"><label for="x">) also works.

Select all

<sg-check controls="x-breakfast x-pickup x-late">
  <label><input type="checkbox" id="x-all"> All extras</label>
</sg-check>
<sg-check><label><input type="checkbox" id="x-breakfast" name="extras" value="breakfast"> Breakfast</label></sg-check>
...

Give the "select all" input no name, so it is not sent with the form. Without JavaScript it is an ordinary checkbox that does nothing, so for a page that must work without scripts, leave it out or render it only when scripts run.

Attributes and properties

ValuesDefault
indeterminateboolean attributeabsent
controlsspace-separated ids of child checkboxesnone
seedany string, for the drawn marksthe label
registerquiet, warm, playfulinherited
checked (property)boolean; setting it fires changethe input's

Registers

LookMotion
quiet (and no JS)hairline box, crisp tick, a bar for "some"the tick scales in under 200ms
warma box sketched in four pencil strokes whose corners cross, a heavy pencil tick, a pencil dashthe tick is drawn in over 280 ms when ticked
playfula rubber-stamp block (a heavy inked edge and a fine inner rule); a bold stamped tick, cut bigger than the box and printed through the Wave 1 stamp's ink texture, with ink spotsthe stamp presses down past flat and settles

Reduced motion: marks appear with no movement. Forced colours: the system checkbox.

Accessibility

  • Keyboard: Tab to it, Space to tick. Nothing added, nothing taken away.
  • A screen reader hears the label and "checked", "not checked" or "mixed".
  • Required: use required on the input. The browser stops the form and says why.
  • Contrast: box edge, pencil and tick are 3:1 or more on every surface (WCAG 1.4.11), tested for every palette and theme.

Budget

Behaviour 8.6 KB of 12 KB (check.js + check.core.js); skins 0.3, 2.2 and 1.9 KB.