Checkbox
A hundred and twenty variants on six axes, plus a ready-made group - every state a tick box can reach, in both the bare and the boxed layout.
A hundred and twenty, not a hundred and sixty
Two by five by two by two by two by two is a hundred and sixty. The file publishes a hundred and twenty, because Show Description only exists where Show Label is Yes. A description with nothing to describe is not a state worth drawing, so those forty are absent from the dropdown rather than present and broken.
| Axis | Values | Multiplier |
|---|---|---|
| Checked | No · Yes | × 2 |
| State | Default · Focus · Invalid · Pressed · Disabled | × 5 |
| Type | Default · Box | × 2 |
| Control Placement | Start · End | × 2 |
| Show Label · Show Description | No/No · Yes/No · Yes/Yes, forty variants each | × 3 |
| Checkbox | the whole container | 120 |
Checkbox Group is published alongside it as a single component rather than a variant: six rows stacked in a column at a 12px gap, 219 wide, label and description on each. It is the shape a real group takes often enough to be worth having ready, and you delete the rows you do not need.
Five states, three mechanisms
Focus and Invalid add a ring. Pressed and Disabled drop opacity, on the control alone and never on the whole row. Invalid is the only state that touches the label. Nothing here repaints the box, so a ticked checkbox stays visibly ticked in every state it can reach.
| State | Unchecked | Checked | What moves |
|---|---|---|---|
| Default | Label | Label | nothing, this is the base |
| Focus | Label | Label | 2px ring outside the 1px stroke |
| Invalid | Label | Label | stroke and ring both go red, and so does the label |
| Pressed | Label | Label | control drops to 60% opacity |
| Disabled | Label | Label | control to 50%, label to 70% |
The Box wrapper sits at a plain 1px border with no fill until the row is picked, then takes base.primary at 5% with the border moving to #B9B9B9. Tint it from the start and a group of four all looks selected, because the eye has nothing to compare against. The tint is the answer to a question, not the question itself.
Disabled dims the control to 50% but the label only to 70%, and the description not at all. Dimming the text to match would make the row unreadable, and a locked setting still has to say what it is. Pressed leaves both text colours alone for the same reason.
Seven rows, all of them tickable
The second frame on the page is 900 by 1180 and holds these seven. The copy is the file's, and every box below is live, so the Box group and the settings row behave here the way they are meant to behave in a product.
Control Placement is a side, and the side moves
This is the axis direction actually touches. Start means the control comes before the label in reading order, so under dir=rtl it sits on the right - the variant name stays true and the pixels move. The box itself is symmetrical and the tick reads the same either way.
No mirrored variants are needed and none should be added: a hundred and twenty would become two hundred and forty for something dir already does. Switch is the one sibling where direction changes more than position - there the knob has to travel the other way, or on and off swap meaning.
One container, a hundred and twenty variants
Read straight out of the Penpot file. The axes below are the property names you will see in the variant dropdown.
Using the Checkbox
Because Show Description only exists where Show Label is Yes. Six axes multiply out to a hundred and sixty, but a description with no label above it is not a shape anyone needs, so those forty are left out of the container rather than drawn and quietly broken.
A selectable card: the whole row gets a 1px border at radius 8 with 10px of padding, and tints to base.primary at 5% once it is chosen. It is the shape you want for a delivery option or a plan, where the target should be the row and not the 16px square.
Because a designer laying out a flow needs to show the moment of the tap, and the file draws it as the control at 60% opacity. It is a design-time state, not something you wire up. Disabled uses the same mechanism at 50%, with the label at 70%.
A settings row, where the label reads as the sentence and the control sits at the far edge of the form. The file keeps the same 8px gap in both directions and only reverses the child order, so nothing about the row's height or alignment changes.
Yes. Fills, strokes and the Box wrapper's radius are all bound to the semantic set, so a Dark theme switch carries the whole component. Two things stay as raw hex: the 4px radius on the 16px control, because no token holds that value, and the focus ring, because the same shape already binds strokeColor to base.input or base.primary for its 1px edge and Penpot allows one colour token per attribute. The ring's hex matches base.ring exactly, it just cannot say so.