PlCheckbox
A single yes/no, or one member of a set of them. The box is a small pane of clear glass until it is ticked, and then it is the colour family's gradient.
import { PlCheckbox } from 'plass-ui';
<PlCheckbox label="Email me about releases" defaultChecked />;import 'package:plass_ui/plass_ui.dart';
PlCheckbox(
value: subscribed,
onChanged: (bool next) => setState(() => subscribed = next),
label: const Text('Email me about releases'),
);Props
| Prop | Type | Default | Description |
|---|---|---|---|
| sizeshared | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'md' | The size of the tick box and the type scale of the text beside it |
| colorshared | 'primary' | 'secondary' | 'success' | 'warning' | 'danger' | 'info' | 'primary' | The gradient the box fills with when it is ticked |
| label | ReactNode | — | The text beside the tick. Wired to it by Base UI's Field, so pressing it ticks the box |
| description | ReactNode | — | Helper text under the label |
| error | ReactNode | — | Error message below. Its presence also turns the checkbox invalid |
| invalid | boolean | — | Forces the invalid state without a message. Defaults to whether error has content |
| checked | boolean | — | The checked state. Use with onCheckedChange for a controlled checkbox |
| defaultChecked | boolean | false | The starting state, uncontrolled |
| onCheckedChange | (checked: boolean, details) => void | — | Called with the new state |
| indeterminate | boolean | false | Neither ticked nor cleared — the third state, announced as aria-checked="mixed" |
| readOnly | boolean | false | The state is shown but cannot be changed |
| disabled | boolean | false | Unavailable. Loses its saturation, lets the page through, and leaves the tab order |
| name · value · required | string · string · boolean | — | For a native form submission. Passed straight to Base UI |
| classNames | { label?, control?, description?, error?: string } | — | Classes on the parts a className does not reach. control is the part a reader acts on |
| Prop | Type | Default | Description |
|---|---|---|---|
| value * | bool | — | Whether the box is ticked. Controlled only: a widget that owned a copy of your state would be a widget your state could disagree with |
| onChanged | ValueChanged<bool>? | — | Called with what the value should become. Leaving it null disables the checkbox, as it does everywhere else in Flutter |
| sizeshared | PlassSize | PlassSize.md | The size of the tick box and the type scale of the text beside it |
| colorshared | PlassColor | PlassColor.primary | The gradient the box fills with when it is ticked |
| label | Widget? | — | The text beside the tick. Wired to it by Base UI's Field, so pressing it ticks the box |
| description | Widget? | — | Helper text under the label |
| error | Widget? | — | Error message below. Its presence also turns the checkbox invalid |
| invalid | bool? | — | Forces the invalid state without a message. Defaults to whether error has content |
| indeterminate | bool | false | Neither ticked nor cleared — the third state, announced as aria-checked="mixed" |
| readOnly | bool | false | The state is shown but cannot be changed |
| disabled | bool | false | Unavailable. Loses its saturation, lets the page through, and leaves the tab order |
| semanticLabel | String? | — | The name a screen reader announces, for a checkbox with no visible label |
Every other prop on Base UI's Checkbox.Root passes straight through. className and style land on the field wrapper rather than on the tick, and render is not offered. Replacing the tick would leave something that is no longer a checkbox.
classNames reaches the four parts inside that wrapper: label, control (the tick) description and error.
The checkbox is controlled: it is handed a value and reports what the value should become. There is no uncontrolled form and no defaultChecked, Flutter's own controls work this way, and a widget that owned a copy of your state would be a widget your state could disagree with.
onChanged: null disables the checkbox, as it does everywhere else in Flutter.
There is no variant. On and off are not two strengths of one material, so the box swaps its whole surface rather than shifting a step along a ladder. The one place in the library a state is expressed that way.
The tick draws itself on rather than appearing whole on the frame the box fills, and draws itself back off when the box is cleared. A mark that arrives all at once is a mark that was swapped in rather than one the click put there. Nothing is scaled to do it. The stroke is dashed at its own length and the dash is what moves, so no part of the tick is ever anywhere it will not end up. See motion.
What the shared axes (size color) mean across the library is in prop conventions.
Examples
color
Ticked, the box fills with the family's gradient and the mark on it is the family's own on-solid ink, which is the value the contrast was measured against.
size
The tick has its own ladder rather than a step off the control heights: it is an indicator beside a label rather than a control with one inside, so it is sized against the text beside it. It also takes a much tighter radius. --plass-radius-md on an 18px box is most of the way to a circle, and a checkbox that is round is a radio button.
indeterminate
The third state, for a parent box over a set of children: neither ticked nor cleared. The mark becomes a dash, and the box is announced as mixed rather than as checked.
It is a display state, not a value, pressing an indeterminate box ticks it.
readOnly · disabled · error
error also turns the checkbox invalid, which re-points the whole colour family at danger, the box, the ring and the message turn over together.
Accessibility
- Base UI renders a
role="checkbox"control witharia-checkedand, withname, the hidden input that makes it part of a native form submission. label,descriptionanderrorare wired to the control by Base UI's Field, so pressing the label ticks the box and a screen reader reads all three together.- The tick is centred on the label's first line with
1lh, so it stays put when the label wraps to three. indeterminateis announced asaria-checked="mixed", and the dash rather than the check is what says so without colour.- The focus ring only appears on
:focus-visible, so a mouse press never draws one. - A checkbox with no
labelneeds anaria-label. A box with nothing beside it is a box nobody can name.
- The tick, its label, its description and its error are one semantics node, checked or not, so a screen reader reads the whole thing once rather than four times.
- Pressing the label ticks the box: the whole row is the target, not the 18px square.
- The tick is centred on the label's first line (a box the height of one line box, whatever the type scale turns out to be), so it stays put when the label wraps to three.
indeterminateis announced as mixed, and the dash rather than the check is what says so without colour.- Enter, Space and the numpad Enter tick it. The focus ring only appears on what CSS calls
:focus-visible, a keyboard reaching the control, never a pointer clicking it. - A checkbox with no
labelneeds asemanticLabel. A box with nothing beside it is a box nobody can name.
Differences from the React build
| React | Flutter | Why |
|---|---|---|
defaultChecked / checked | value and onChanged | Flutter's own controls are controlled. A widget that owned a copy of your state would be a widget your state could disagree with. |
onCheckedChange | onChanged | Flutter's name. onChanged: null disables the checkbox, as it does everywhere else. |
name, and a hidden input | — | There is no native form submission to be part of. |
Base UI's Field wiring | one merged semantics node | The same result by a different route: label, description and error are read together because they are one node. |
aria-label | semanticLabel | Flutter's name. |
className, style | — | There is no class list and no style attribute to pass through. |