PlSwitch
An immediate on/off. The track is a neutral groove while it is off, and the colour family's gradient once it is on.
import { PlSwitch } from 'plass-ui';
<PlSwitch label="Dark mode" checked={dark} onCheckedChange={setDark} />;import 'package:plass_ui/plass_ui.dart';
PlSwitch(
value: dark,
onChanged: (bool next) => setState(() => dark = next),
label: const Text('Dark mode'),
);Props
| Prop | Type | Default | Description |
|---|---|---|---|
| sizeshared | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'md' | The size of the track and the type scale beside it |
| colorshared | 'primary' | 'secondary' | 'success' | 'warning' | 'danger' | 'info' | 'primary' | The gradient the track fills with when it is on |
| label | ReactNode | — | The text beside the track. Wired to it by Base UI's Field, so pressing it flips the switch |
| description | ReactNode | — | Helper text under the label |
| error | ReactNode | — | Error message below. Its presence also turns the switch invalid |
| invalid | boolean | — | Forces the invalid state without a message. Defaults to whether error has content |
| labelPlacementshared | 'start' | 'end' | 'end' | Which side the label sits on. start is for a settings list, where the labels form a column and the switches line up |
| checked | boolean | — | The on state. Use with onCheckedChange for a controlled switch |
| defaultChecked | boolean | false | The starting state, uncontrolled |
| onCheckedChange | (checked: boolean, details) => void | — | Called with the new state |
| 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 switch is on. Controlled only |
| onChanged | ValueChanged<bool>? | — | Called with what the value should become. Leaving it null disables the switch |
| sizeshared | PlassSize | PlassSize.md | The size of the track and the type scale beside it |
| colorshared | PlassColor | PlassColor.primary | The gradient the track fills with when it is on |
| label | Widget? | — | The text beside the track. Wired to it by Base UI's Field, so pressing it flips the switch |
| description | Widget? | — | Helper text under the label |
| error | Widget? | — | Error message below. Its presence also turns the switch invalid |
| invalid | bool? | — | Forces the invalid state without a message. Defaults to whether error has content |
| labelPlacementshared | PlassAlign | PlassAlign.end | Which side the label sits on. PlassAlign.center asserts: a switch label sits at one end of a row or the other |
| 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 switch with no visible label |
Every other prop on Base UI's Switch.Root passes straight through. className and style land on the field wrapper rather than on the track, and render is not offered.
classNames reaches the four parts inside that wrapper: label, control (the track) description and error.
The switch is controlled, like every other control in the package: it is handed a value and reports what the value should become. onChanged: null disables it.
There is no variant, for the reason a PlCheckbox has none: on and off are not two strengths of one material.
What the shared axes (size color) mean across the library is in prop conventions.
Switch or checkbox
The difference is temporal. A checkbox is a value that gets submitted with a form; a switch takes effect the moment it moves. If there is a Save button underneath, it should have been a checkbox.
Examples
color
On, the track is the family's gradient with that family's tinted shadow under it. Off, it is the groove. The same neutral ink a PlSlider's rail is, so the two controls in a settings panel are visibly made of the same thing.
The thumb is white in both states and in both themes: it is the light on the track, not a second coloured object, and a coloured thumb on a coloured track is two things fighting over sixteen pixels.
There is no inset shadow under the off track and no hairline round it. An off state drawn as the glass at its most opaque is a white pill with a white thumb in it, which on a light page is a switch nobody can find until they have already flipped it, and where it was visible, in the dark, a recessed slot under a domed thumb was the moulded rocker this design language exists not to draw.
size
The thumb is inset 2px on every side, so its diameter is the track's height minus four at every step and the two never drift apart.
labelPlacement
endPlassAlign.end (the default) reads as a caption for the control. startPlassAlign.start is for a settings list: the labels form a column and every switch lines up against the trailing edge of the row.readOnly · disabled
Accessibility
- Base UI renders a
role="switch"control witharia-checked, and withnamethe 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 flips the switch.- Space and Enter both flip it; the focus ring appears only on
:focus-visible. - The thumb's position is not the only signal. The track changes material as well, so the state survives a reader who cannot tell the two ends of a 36px pill apart.
- The thumb is the one thing in the library that moves, and it carries no text. The no-transform rule is about a control resampling its own label under the finger, which this cannot do. It travels in one house duration, the same 150ms everything else changes in.
- A switch with no
labelneeds anaria-label.
- The track, its label and its description are one semantics node, announced as toggled or not.
- Pressing the label flips the switch: the whole row is the target.
- Enter, Space and the numpad Enter flip it; the focus ring only appears on what CSS calls
:focus-visible. - The thumb's position is not the only signal. The track changes material as well, so the state survives a reader who cannot tell the two ends of a 36px pill apart.
- The thumb is the one thing in the library that moves, and it carries no text. It travels in one house duration, the same 150ms everything else changes in.
- A switch with no
labelneeds asemanticLabel.
Differences from the React build
| React | Flutter | Why |
|---|---|---|
checked / onCheckedChange | value / onChanged | Flutter's names, and onChanged: null disables the switch as it does everywhere else. |
name, and a hidden input | — | There is no native form submission to be part of. |
labelPlacement="start" | labelPlacement: PlassAlign.start | The same value out of the shared vocabulary; PlassAlign.center asserts, because a switch label sits at one end of a row or the other. |
aria-label | semanticLabel | Flutter's name. |
className, style | — | There is no class list and no style attribute to pass through. |