Checkbox
The product's only on and off control, and it is a native input rather than a drawn switch. It carries the four preferences on Settings and the two on Data and privacy, which is to say it carries every promise this product makes about what it will tell a person and what it will hold.
Anatomy
One box and one value, which is what makes it an atom. Everything below is drawn by design/system/components/checkbox.css through the same entry point a product screen loads, so this page cannot drift from the screens: there is no second copy of the CSS here to drift from.
.checkthe atom itself: a realinput type="checkbox"at 20px by 20px, wearing the class step 6 gave it. There is no custom switch anywhere in the 57 grey pages and none is proposedaccent-colorreads--bg-action. It is the one colour hook a native checkbox gives, and it paints the fill of the checked box and the tick inside itflex: noneso the row's flex box cannot stretch the control when the label beside it wraps to two linesmargin: 3px 0 0drops the box onto the first line of the label text beside it instead of the top of the text stacklabel.switchnot this atom. It is the Switch row molecule, written in switch-row.css, which is where the rows above get their 16px of vertical padding and the hairline between them. It is also the real pointer target, and that matters below
The box, its edge, its corner and its tick are drawn by the browser. Nothing in tokens.css reaches them except through accent-color, and reaching them properly means appearance: none and rebuilding the control out of a pseudo-element, which is a redraw and not a move. This stage does not move a pixel.
Variants and sizes
The checkbox has one form, so there is no matrix to draw. All six instances in the product render identically at 20px, and the census measured .switch input as one of the sixteen families with a single value on every property, at both viewports. Checked against unchecked is not a variant: it is the control's value, and the difference is drawn by the browser out of accent-color. On an unchecked box no token of ours is visible at all.
The 20px, and why it is not being fixed here
The control is 20px and WCAG 2.5.8 asks 24px. This is a named carry, recorded twice in docs/decisions.md on 2026-08-11, and it is on this page for the same reason it is in that file: a defect that is carried silently is a defect that ships.
| The question | The answer, and where it is written |
|---|---|
| What does 2.5.8 measure? | The target, meaning the whole area a pointer may land on to operate the control. Here the control sits inside label.switch, and a click anywhere in that label toggles it |
| How big is the real target? | 44px or more, per the decision of 2026-08-11. The Settings rows carry two lines of text, a 12px top and bottom padding and a divider, so they stand well clear of it. The raw 20px box is not the target a person hits |
| Why not simply set it to 24px? | Because 20px is a kit value, and this stage changes a value only by a named decision, never as a side effect of moving a component into a file. A resize here would move pixels on Settings and on Data and privacy, and the pixel comparison at step 8 would report a difference with no line in any of the three lists that are allowed to produce one |
| So what closes it? | A founder decision, at the responsive stage or before it, with a number and a reason. Until then it stands here, in docs/decisions.md and in docs/inventory.md, in the same words in all three places |
There is no switch component in this product, native or drawn, and the class name .switch on the row is a name rather than a control: the row is called a switch row and what it holds is a checkbox. That is worth knowing before somebody builds a toggle because the class implied one.
When to use it
A checkbox carries a preference that is on or off and takes effect where it stands. All six in the product answer one of two questions. What may Tendd tell you about: "A price goes up", "A payment does not go through", "A free trial is ending soon", "Weekly email digest", the four on Settings. And what may Tendd do with your data: "Refresh my bank data automatically", "Use my activity to improve Tendd", the two on Data and privacy. Each is a sentence a person agrees with rather than a setting they configure, which is why the row shows the sentence and a plain-language example underneath it.
Both positions have to be real. The grey original of Data and privacy states the rule in its own comment: "Two real toggles and no theatre. There is no advertising switch and no personalisation switch, because we do neither, and offering a control with only one legal position implies we could." A checkbox is a claim that off is available. Offering one that cannot be turned off, or that turns off nothing, spends the trust this product is built on to look configurable.
Nothing about it shouts. The tick takes petrol, which is one of the accent's four permitted jobs read as a current selection, and the row around it is a hairline divider and two tiers of grey text. There is no count, no red, no "recommended" badge and no default marked as better than the other: a preference screen that argues with a person is a preference screen they stop trusting, and this product's whole claim is that it says what it does with the data every time.
The rule, and the anti-rule
A preference with two states, both of them visible at once, that takes effect where it stands and needs no submit. The person reads the sentence, sees the answer, and changes it or does not.
An action that commits or leaves the screen belongs to the Button. A checkbox that means "yes, do the thing" is a control whose state is a promise the product has not kept yet, and a person then hunts for the button that makes it real. If ticking it starts something, it was never a preference.
Four independent yes or no questions on Settings, each answerable without touching the others. Stacked in a divided list, they read as a paragraph about what the product will say to you.
One answer out of three or more is the Select. Checkboxes that are secretly exclusive are the oldest bug in forms: nothing stops a person ticking two, so the product has to untick one for them, and an interface that undoes what a person just did is the opposite of calm.
Limits
U13, context. PRESS, and this is the one place the nudge is a SCALE. A 20px box cannot travel 2px without looking dislodged, and the box IS the control rather than a part of one, so the press reads as the box yielding under the finger: it shrinks to 0.94, the mirror of the merchant mark’s 1.06 lift. The tick is drawn by the platform through accent-color and cannot be animated at all. Everything in this product that moves travels exactly one --nudge, which is 2px, and nothing anywhere travels two. Hover and release run at --dur-state (150ms) on --ease-arrive; a press runs at --dur-press (90ms), because it answers a finger that is already down. The full language and the live specimens are on Motion; the table with sources is Usage rules.
States
Four states, and they exist because the checkbox is interactive. Each one is a token override rather than a style of its own, and on this control the overrides are unusually thin: everything a state can reach on a native checkbox is accent-color and the focus ring, because the box and the tick are the browser's.
Two of the four are authored, and both move zero pixels today. kit.css gave the checkbox no hover and no disabled rule. Hover follows the filled button exactly, one step deeper into the accent through a token; disabled repaints from --bg-recessed, the same role the disabled button uses. No page carries a disabled checkbox, so nothing on any screen changes. The one place that looks like it should is "A free trial is ending soon" on Settings, which is a Pro feature on a Free account, and the product deliberately leaves it enabled and unchecked rather than locking a person out of reading it.
The hover is on the control, not on the row. The row is label.switch and belongs to the Switch row molecule; an atom that repaints its host is the unnamed variant this stage exists to prevent. If the row should light up under the pointer, that decision belongs to switch-row.css and to the person who owns it.
Rest, live
The third one is genuinely disabled, by the attribute rather than by a class the product does not carry. Faking a state with .is-disabled on a native control would document something the product cannot do, and it would also lie to a screen reader, which reads the attribute and not the class.
The four, in both themes
The states are described, not photographed. Founder's decision, 2026-08-11: the snapshot strip went from every page in this kit, 120 images across 18 pages. They were framed as measurements rather than illustrations, and that framing was honest, but the bill came due all session: a value moves, and every picture of it is quietly stale until somebody re-shoots it. A stale picture is worse than no picture, because it looks checked. What is left is what a picture never carried anyway: the state, and the token it reads. The resting component is live and interactive above, so hover it, tab to it, and read the value off the real thing rather than off a photograph of it.
--bg-action through accent-color, and on the unchecked box nothing of ours at all--bg-action-strong. AUTHORED, and visible only on a checked box: an empty box has no fill of ours to deepen--color-focus, 6.23:1 as a line. It matters more here than on a wide control: the row is not focusable, so this 2px ring on a 20px square is the whole of the keyboard's feedback--color-focus, 6.82:1. The ring is 2px at 2px offset and sits outside the box, so a focused checkbox moves no neighbour and the row does not jump--bg-recessed, the same role the disabled button fills with. AUTHORED, and used by no screenRetake, and this is the whole recipe, because there is no node Playwright in this repo and a
command that does not run is worse than no command:
serve the repo, then for theme in (none, data-theme="dark" on <html>):
goto /design/kit/checkbox.html
move the pointer away -> screenshot #live-rest -> screens/checkbox-rest-<theme>.png
hover #shot-on -> screenshot #live-rest -> screens/checkbox-hover-<theme>.png
focus #shot-on, press Tab, assert :focus-visible -> screenshot #live-rest
-> checkbox-focus-<theme>.png
blur, screenshot #live-rest -> the disabled one is already in frame
-> screens/checkbox-disabled-<theme>.png
Shoot the frame at 2x or the 20px box arrives as mush and the states cannot be told apart.
The states are reached the way a person reaches them: a real pointer, a real Tab, a real
disabled attribute. NO FILE COMES OUT OF THIS TODAY, and the recipe is kept for the day one is
wanted rather than as a promise. The strip was removed by the founder on 2026-08-11 and the
states above are described instead, so there is nothing here for step 9 to diff. The earlier
wording said step 9 checks these files for a byte shift; it was written before the decision
and outlived it, which is the same freeze it was warning about, in prose.
The technical half
| Reads | Which token | Growing from |
|---|---|---|
| the checked fill and its tick | --bg-action, through accent-color | --petrol |
| the same under the pointer | --bg-action-strong | --petrol-deep, and --petrol-deep-dark in the dark theme, which is lighter and not darker |
| the focus ring | --color-focus | --petrol |
| disabled | --bg-recessed | --panel, a role that already existed |
| the box, its edge, its corner, its tick shape | none | the browser. accent-color is the whole surface this system has on a native checkbox |
| its size | none | 20px, a literal and a named carry against the 24px of WCAG 2.5.8. See the block above |
Lives in design/system/components/checkbox.css. Stands on 2 grey pages and 6 checkboxes: settings, four of them, and data-privacy, two. Coloured screens carrying it: Settings, which holds all four. Data and privacy is still grey, so its two arrive with the rollout.
<label class="switch">
<input type="checkbox" checked>
<span><span class="t">A price goes up</span><span class="h">Like "Netflix went up by $2.50".</span></span>
</label>
The input has no id and the label has no for, and that is correct rather than sloppy: the label wraps the control, which associates them and makes the whole row the target. It is also what the 20px carry rests on.