Components/Checkbox

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.

Checkbox · Components frameLive in the browser · drawn to match the file
Checked
State
Type
Placement
Label
Description
Checkbox Text This is a checkbox description.
Checked=Yes · State=Default · Type=Default · Placement=Start219 × 40 in the file
Six axes, one container

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.

AxisValuesMultiplier
CheckedNo · Yes× 2
StateDefault · Focus · Invalid · Pressed · Disabled× 5
TypeDefault · Box× 2
Control PlacementStart · End× 2
Show Label · Show DescriptionNo/No · Yes/No · Yes/Yes, forty variants each× 3
the whole container

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.

What each state actually changes

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.

StateUncheckedCheckedWhat moves
LabelLabelnothing, this is the base
FocusLabelLabel2px ring outside the 1px stroke
InvalidLabelLabelstroke and ring both go red, and so does the label
PressedLabelLabelcontrol drops to 60% opacity
DisabledLabelLabelcontrol to 50%, label to 70%
why Box tints only once it is chosen

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.

the detail that is easy to miss

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.

The usage frame

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.

Consent - one checkbox that has to be ticked
Invalid - the same field after a failed submit
With a description - extra context under the label
Disabled - locked by an organisation policy
Weekly digest email
Group - the topics a reader can follow
Group - Box type, one option chosen
Control at the end - a settings row
Right to left

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.

dir=rtlLive in the browser · drawn to match the file
Direction
Start sits on the left · End on the right

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.

In the file

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.

CheckboxCheckedNo · Yes× 2
CheckboxStateDefault · Focus · Invalid · Pressed · Disabled× 5
CheckboxTypeDefault · Box× 2
CheckboxControl PlacementStart · End× 2
CheckboxShow Label × Show DescriptionNo/No · Yes/No · Yes/Yes - the fourth pairing is not drawn× 3
Checkbox Groupnonesix rows in a column at a 12px gap, 219 wide1

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.

Forms · 120 variants