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.
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>;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
| Prop | Type | Default | Description |
|---|---|---|---|
| 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 |
| elevationshared | 0 | 1 | 2 | 3 | 0 | Drop 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 |
| pressed | boolean | — | Whether it is on. Use with onPressedChange for a controlled toggle |
| defaultPressed | boolean | false | Whether it starts on, for an uncontrolled one |
| onPressedChange | (pressed: boolean) => void | — | Called when it goes on or off |
| value | string | — | Identifies the toggle inside a PlToggleGroup |
| startIcon | ReactNode | — | Content placed before the label. Sized in em, so it tracks the label |
| endIcon | ReactNode | — | Content placed after the label |
| fullWidth | boolean | false | Stretches to the width of the container |
| disabled | boolean | false | Makes it unpressable and takes it out of the tab order |
| children | ReactNode | — | The label. Left out, the toggle goes square around whatever icon it was given — which still needs an aria-label |
| Prop | Type | Default | Description |
|---|---|---|---|
| variantshared | PlassVariant? | PlassVariant.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 | PlassSize? | PlassSize.md | Height and type scale |
| colorshared | PlassColor? | PlassColor.primary | Semantic colour role. Arbitrary colour values are not accepted |
| densityshared | PlassDensity? | PlassDensity.standard | Padding only — never the height, never the type scale |
| elevationshared | int? | 0 | Drop 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 |
| pressed | bool? | — | Whether it is on. Use with onPressedChange for a controlled toggle |
| defaultPressed | bool | false | Whether it starts on, for an uncontrolled one |
| onPressedChanged | ValueChanged<bool>? | — | Called when it goes on or off |
| value | String? | — | Identifies the toggle inside a PlToggleGroup |
| startIcon | Widget? | — | Content placed before the label. Sized in em, so it tracks the label |
| endIcon | Widget? | — | Content placed after the label |
| fullWidth | bool | false | Stretches to the width of the container |
| disabled | bool? | false | Makes it unpressable and takes it out of the tab order |
| semanticLabel | String? | — | The name a screen reader gives it. Required in practice on a toggle with an icon and no label |
| focusNode | FocusNode? | — | An external focus node, for a caller that has to move focus here itself |
| autofocus | bool | false | Whether it takes focus when it is first built |
| child | Widget? | — | 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
| Prop | Type | Default | Description |
|---|---|---|---|
| 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 |
| elevationshared | 0 | 1 | 2 | 3 | 0 | Drop shadow depth, passed to every toggle in the set |
| value | readonly 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 |
| defaultValue | readonly string[] | — | Which start on, for an uncontrolled set |
| onValueChange | (value: string[]) => void | — | Called when the set's value changes |
| multiple | boolean | false | Whether 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 |
| disabled | boolean | — | Disables every toggle in the set at once |
| loopFocus | boolean | true | Whether the arrow keys wrap around at the ends |
| fullWidth | boolean | false | Stretches to the container and divides the width evenly between toggles |
| children | ReactNode | — | The PlToggles that make up the set |
| Prop | Type | Default | Description |
|---|---|---|---|
| children * | List<Widget> | — | The toggles that make up the set. Each needs a value to be part of it |
| value | List<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 |
| defaultValue | List<String> | <String>[] | Which start on, for an uncontrolled set |
| onValueChanged | ValueChanged<List<String>>? | — | Called when the set's value changes |
| multiple | bool | false | Whether more than one can be on at a time. Off, turning one on turns the last one off |
| orientationshared | PlassOrientation | PlassOrientation.horizontal | Which way the toggles run |
| variantshared | PlassVariant? | 'glass' | Passed to every toggle in the set |
| sizeshared | PlassSize? | 'md' | Height and type scale |
| colorshared | PlassColor? | 'primary' | Semantic colour role. Arbitrary colour values are not accepted |
| densityshared | PlassDensity? | 'default' | Padding only — never the height, never the type scale |
| elevationshared | int? | 0 | Drop shadow depth, passed to every toggle in the set |
| disabled | bool? | — | Disables every toggle in the set at once |
| fullWidth | bool | false | Stretches 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
PlSwitchchanges a setting, and the change is the point. - A
PlCheckboxis an answer in a form rather than a control. - A
PlSegmentedButtonor aPlRadioGroupis what a one-of-a-set value is. APlToggleGroupwithoutmultiplelooks 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.
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.
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.
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.
Differences from the React build
| React | Flutter | Why |
|---|---|---|
aria-pressed from Base UI | Semantics(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 it | one focus stop per toggle | Base 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 freely | children: List<Widget> on the group | The group has to know which member is at each end to square off the right corners, and a list is what it can count. |
loopFocus | — | There is no roving focus to wrap. |
onPressedChange | onPressedChanged | Flutter's name. |
aria-label | semanticLabel | Flutter's name. |
className, style, native attributes | — | There is no class list and no style attribute to pass through. |
Accessibility
- Base UI renders a real
<button>witharia-pressed, which is what says "this is a state" rather than "this does something". - A
PlToggleGroupis 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.loopFocusdecides whether the arrows wrap at the ends. - An icon-only toggle needs an
aria-label. Nothing else can name it. disabledtakes the toggle out of the tab order. A group'sdisableddoes 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 claimaria-pressedmakes on the other side. - A toggle with an icon and no label needs a
semanticLabel. Nothing else can name it. disabledtakes 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.