Skip to content

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.

React
tsx
import { PlCheckbox } from 'plass-ui';

<PlCheckbox label="Email me about releases" defaultChecked />;
dart
import 'package:plass_ui/plass_ui.dart';

PlCheckbox(
  value: subscribed,
  onChanged: (bool next) => setState(() => subscribed = next),
  label: const Text('Email me about releases'),
);

Props

PropTypeDefaultDescription
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
labelReactNodeThe text beside the tick. Wired to it by Base UI's Field, so pressing it ticks the box
descriptionReactNodeHelper text under the label
errorReactNodeError message below. Its presence also turns the checkbox invalid
invalidbooleanForces the invalid state without a message. Defaults to whether error has content
checkedbooleanThe checked state. Use with onCheckedChange for a controlled checkbox
defaultCheckedbooleanfalseThe starting state, uncontrolled
onCheckedChange(checked: boolean, details) => voidCalled with the new state
indeterminatebooleanfalseNeither ticked nor cleared — the third state, announced as aria-checked="mixed"
readOnlybooleanfalseThe state is shown but cannot be changed
disabledbooleanfalseUnavailable. Loses its saturation, lets the page through, and leaves the tab order
name · value · requiredstring · string · booleanFor 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
PropTypeDefaultDescription
value * boolWhether the box is ticked. Controlled only: a widget that owned a copy of your state would be a widget your state could disagree with
onChangedValueChanged<bool>?Called with what the value should become. Leaving it null disables the checkbox, as it does everywhere else in Flutter
sizesharedPlassSizePlassSize.mdThe size of the tick box and the type scale of the text beside it
colorsharedPlassColorPlassColor.primaryThe gradient the box fills with when it is ticked
labelWidget?The text beside the tick. Wired to it by Base UI's Field, so pressing it ticks the box
descriptionWidget?Helper text under the label
errorWidget?Error message below. Its presence also turns the checkbox invalid
invalidbool?Forces the invalid state without a message. Defaults to whether error has content
indeterminateboolfalseNeither ticked nor cleared — the third state, announced as aria-checked="mixed"
readOnlyboolfalseThe state is shown but cannot be changed
disabledboolfalseUnavailable. Loses its saturation, lets the page through, and leaves the tab order
semanticLabelString?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.

React

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.

React

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.

React

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.

React

Accessibility

  • Base UI renders a role="checkbox" control with aria-checked and, with name, the hidden input that makes it part of a native form submission.
  • label, description and error are 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.
  • indeterminate is announced as aria-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 label needs an aria-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.
  • indeterminate is 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 label needs a semanticLabel. A box with nothing beside it is a box nobody can name.

Differences from the React build

ReactFlutterWhy
defaultChecked / checkedvalue and onChangedFlutter's own controls are controlled. A widget that owned a copy of your state would be a widget your state could disagree with.
onCheckedChangeonChangedFlutter's name. onChanged: null disables the checkbox, as it does everywhere else.
name, and a hidden inputThere is no native form submission to be part of.
Base UI's Field wiringone merged semantics nodeThe same result by a different route: label, description and error are read together because they are one node.
aria-labelsemanticLabelFlutter's name.
className, styleThere is no class list and no style attribute to pass through.

Released under the MIT License