Skip to content

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.

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

<PlSwitch label="Dark mode" checked={dark} onCheckedChange={setDark} />;
dart
import 'package:plass_ui/plass_ui.dart';

PlSwitch(
  value: dark,
  onChanged: (bool next) => setState(() => dark = next),
  label: const Text('Dark mode'),
);

Props

PropTypeDefaultDescription
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
labelReactNodeThe text beside the track. Wired to it by Base UI's Field, so pressing it flips the switch
descriptionReactNodeHelper text under the label
errorReactNodeError message below. Its presence also turns the switch invalid
invalidbooleanForces 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
checkedbooleanThe on state. Use with onCheckedChange for a controlled switch
defaultCheckedbooleanfalseThe starting state, uncontrolled
onCheckedChange(checked: boolean, details) => voidCalled with the new state
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 switch is on. Controlled only
onChangedValueChanged<bool>?Called with what the value should become. Leaving it null disables the switch
sizesharedPlassSizePlassSize.mdThe size of the track and the type scale beside it
colorsharedPlassColorPlassColor.primaryThe gradient the track fills with when it is on
labelWidget?The text beside the track. Wired to it by Base UI's Field, so pressing it flips the switch
descriptionWidget?Helper text under the label
errorWidget?Error message below. Its presence also turns the switch invalid
invalidbool?Forces the invalid state without a message. Defaults to whether error has content
labelPlacementsharedPlassAlignPlassAlign.endWhich side the label sits on. PlassAlign.center asserts: a switch label sits at one end of a row or the other
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 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.

React

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.

React

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.
React

readOnly · disabled

React

Accessibility

  • Base UI renders a role="switch" 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 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 label needs an aria-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 label needs a semanticLabel.

Differences from the React build

ReactFlutterWhy
checked / onCheckedChangevalue / onChangedFlutter's names, and onChanged: null disables the switch as it does everywhere else.
name, and a hidden inputThere is no native form submission to be part of.
labelPlacement="start"labelPlacement: PlassAlign.startThe 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-labelsemanticLabelFlutter's name.
className, styleThere is no class list and no style attribute to pass through.

Released under the MIT License