Skip to content

PlToggle

A button that stays down, and a set of them that share one state. Off is neutral, because a toggle at rest is not an action waiting to be taken. It is a state that is currently false.

React
tsx
import { PlToggle, PlToggleGroup } from 'plass-ui';

<PlToggle pressed={bold} onPressedChange={setBold}>
  Bold
</PlToggle>;

<PlToggleGroup multiple value={marks} onValueChange={setMarks}>
  <PlToggle value="bold">Bold</PlToggle>
  <PlToggle value="italic">Italic</PlToggle>
</PlToggleGroup>;
dart
import 'package:plass_ui/plass_ui.dart';

PlToggle(
  pressed: bold,
  onPressedChanged: (bool next) => setState(() => bold = next),
  child: const Text('Bold'),
);

PlToggleGroup(
  multiple: true,
  value: marks,
  onValueChanged: (List<String> next) => setState(() => marks = next),
  children: const <Widget>[
    PlToggle(value: 'bold', child: Text('Bold')),
    PlToggle(value: 'italic', child: Text('Italic')),
  ],
);

Props

PropTypeDefaultDescription
variantshared'solid' | 'glass' | 'ghost''glass'What the key is made of while it is **off**. On is always the colour family asserting itself, whichever material was asked for
sizeshared'xs' | 'sm' | 'md' | 'lg' | 'xl''md'Height and type scale
colorshared'primary' | 'secondary' | 'success' | 'warning' | 'danger' | 'info''primary'Semantic colour role. Arbitrary colour values are not accepted
densityshared'default' | 'compact''default'Padding only — never the height, never the type scale
elevationshared0 | 1 | 2 | 30Drop shadow depth. 0 is the default and one below a PlButton's: a toggle is a state rather than an action, and a state does not float off the page waiting to be taken
pressedbooleanWhether it is on. Use with onPressedChange for a controlled toggle
defaultPressedbooleanfalseWhether it starts on, for an uncontrolled one
onPressedChange(pressed: boolean) => voidCalled when it goes on or off
valuestringIdentifies the toggle inside a PlToggleGroup
startIconReactNodeContent placed before the label. Sized in em, so it tracks the label
endIconReactNodeContent placed after the label
fullWidthbooleanfalseStretches to the width of the container
disabledbooleanfalseMakes it unpressable and takes it out of the tab order
childrenReactNodeThe label. Left out, the toggle goes square around whatever icon it was given — which still needs an aria-label
PropTypeDefaultDescription
variantsharedPlassVariant?PlassVariant.glassWhat the key is made of while it is **off**. On is always the colour family asserting itself, whichever material was asked for
sizesharedPlassSize?PlassSize.mdHeight and type scale
colorsharedPlassColor?PlassColor.primarySemantic colour role. Arbitrary colour values are not accepted
densitysharedPlassDensity?PlassDensity.standardPadding only — never the height, never the type scale
elevationsharedint?0Drop shadow depth. 0 is the default and one below a PlButton's: a toggle is a state rather than an action, and a state does not float off the page waiting to be taken
pressedbool?Whether it is on. Use with onPressedChange for a controlled toggle
defaultPressedboolfalseWhether it starts on, for an uncontrolled one
onPressedChangedValueChanged<bool>?Called when it goes on or off
valueString?Identifies the toggle inside a PlToggleGroup
startIconWidget?Content placed before the label. Sized in em, so it tracks the label
endIconWidget?Content placed after the label
fullWidthboolfalseStretches to the width of the container
disabledbool?falseMakes it unpressable and takes it out of the tab order
semanticLabelString?The name a screen reader gives it. Required in practice on a toggle with an icon and no label
focusNodeFocusNode?An external focus node, for a caller that has to move focus here itself
autofocusboolfalseWhether it takes focus when it is first built
childWidget?The label. Left out, the toggle goes square around whatever icon it was given — which still needs an aria-label

Every native <button> attribute passes straight through. color is excluded because it is a Plass prop here, and value because it identifies the toggle in a group rather than being submitted.

PlToggleGroup

PropTypeDefaultDescription
variantshared'solid' | 'glass' | 'ghost''glass'Passed to every toggle in the set
sizeshared'xs' | 'sm' | 'md' | 'lg' | 'xl''md'Height and type scale
colorshared'primary' | 'secondary' | 'success' | 'warning' | 'danger' | 'info''primary'Semantic colour role. Arbitrary colour values are not accepted
densityshared'default' | 'compact''default'Padding only — never the height, never the type scale
elevationshared0 | 1 | 2 | 30Drop shadow depth, passed to every toggle in the set
valuereadonly string[]Which toggles are on, by their value. An array in both cases — the shape that does not change type when multiple is turned on
defaultValuereadonly string[]Which start on, for an uncontrolled set
onValueChange(value: string[]) => voidCalled when the set's value changes
multiplebooleanfalseWhether more than one can be on at a time. Off, turning one on turns the last one off
orientationshared'horizontal' | 'vertical''horizontal'Which way the toggles run
disabledbooleanDisables every toggle in the set at once
loopFocusbooleantrueWhether the arrow keys wrap around at the ends
fullWidthbooleanfalseStretches to the container and divides the width evenly between toggles
childrenReactNodeThe PlToggles that make up the set
PropTypeDefaultDescription
children * List<Widget>The toggles that make up the set. Each needs a value to be part of it
valueList<String>?Which toggles are on, by their value. An array in both cases — the shape that does not change type when multiple is turned on
defaultValueList<String><String>[]Which start on, for an uncontrolled set
onValueChangedValueChanged<List<String>>?Called when the set's value changes
multipleboolfalseWhether more than one can be on at a time. Off, turning one on turns the last one off
orientationsharedPlassOrientationPlassOrientation.horizontalWhich way the toggles run
variantsharedPlassVariant?'glass'Passed to every toggle in the set
sizesharedPlassSize?'md'Height and type scale
colorsharedPlassColor?'primary'Semantic colour role. Arbitrary colour values are not accepted
densitysharedPlassDensity?'default'Padding only — never the height, never the type scale
elevationsharedint?0Drop shadow depth, passed to every toggle in the set
disabledbool?Disables every toggle in the set at once
fullWidthboolfalseStretches to the container and divides the width evenly between toggles

What the shared axes (variant size color density elevation) mean across the library is in prop conventions.

Toggle, switch or checkbox

  • A toggle changes the state of the thing beside it, bold on the selected words, the grid on the canvas, the filter on the list. It is a control, and it never goes in a form.
  • A PlSwitch changes a setting, and the change is the point.
  • A PlCheckbox is an answer in a form rather than a control.
  • A PlSegmentedButton or a PlRadioGroup is what a one-of-a-set value is. A PlToggleGroup without multiple looks like one and is not: what it holds is a state, not an answer.

Examples

variant

What the key is made of while it is off. On is always the colour family asserting itself, whichever material was asked for, and the two answers it gives are the same two a PlSegmentedButton's chosen segment gives: solid takes the gradient and the on-fill ink, glass and ghost light the sheet and leave the label in the accent.

Off, the ink is --plass-muted-fg in all three and none of them is dyed. An off toggle is a piece of clear glass; the family arrives with the press and not before it.

That holds under the pointer as well, which is the easy half to lose. A hover is still the off state, so it climbs the neutral glass ladder. The same two rungs solid and glass were already using, and never the family's own wash, which is what on is painted with. A control with two states cannot draw the false one in the colour of the true one and leave the ink to carry the difference.

React

The elevation

A toggle that is on is not a toggle that is elevated. elevation is the same in both states and only the colour changes, because "on" is a fact about the thing beside the toggle rather than about how far the key is off the page.

It defaults to 0, one below a PlButton's, for the same reason.

size

The control ladder, unchanged: a md toggle is 40px and lines up with the field and the button beside it. density moves the padding and nothing else.

React

PlToggleGroup

Two things are happening and only one of them is visual. The corners facing a neighbour are squared off, which is the visual half. The other half is that the set owns the value: the toggles report into one array, and variant, size, color, density, elevation and disabled are set once on the group rather than on every toggle.

The value is an array in both cases, which is the one shape that does not change type when multiple is turned on.

React

An icon and no label

Left out, children makes the toggle go square around whatever icon it was given, which is what a toolbar toggle is. It still needs an aria-label: a control whose whole label is a drawing has no accessible name at all.

React

Differences from the React build

ReactFlutterWhy
aria-pressed from Base UISemantics(toggled:)The same claim under the framework's own name: a button with a state rather than a button that does something.
one tab stop for the group, arrow keys inside itone focus stop per toggleBase UI's roving tab index has no counterpart in widgets.dart, and a roving focus implemented badly is worse than the platform's own traversal. PlSegmentedButton carries the real thing where a value genuinely needs it.
children composed freelychildren: List<Widget> on the groupThe group has to know which member is at each end to square off the right corners, and a list is what it can count.
loopFocusThere is no roving focus to wrap.
onPressedChangeonPressedChangedFlutter's name.
aria-labelsemanticLabelFlutter's name.
className, style, native attributesThere is no class list and no style attribute to pass through.

Accessibility

  • Base UI renders a real <button> with aria-pressed, which is what says "this is a state" rather than "this does something".
  • A PlToggleGroup is one tab stop with the arrow keys moving between its members, which is what makes a toolbar of eight toggles two key presses deep instead of eight. loopFocus decides whether the arrows wrap at the ends.
  • An icon-only toggle needs an aria-label. Nothing else can name it.
  • disabled takes the toggle out of the tab order. A group's disabled does it to every member at once.
  • The pointer light is off while the toggle is disabled, so a surface nobody can press does not answer the pointer.
  • The toggle is a Semantics(button: true, toggled: …), which is the same claim aria-pressed makes on the other side.
  • A toggle with an icon and no label needs a semanticLabel. Nothing else can name it.
  • disabled takes the toggle out of the focus order and stops it answering the pointer at all. The light goes out with it.
  • Each toggle in a group is its own focus stop. There is no roving focus here, which is the one thing the React build has that this does not.

Released under the MIT License