Skip to content

PlSegmentedButton

Two or more choices in one pill, exactly one of them taken. The tile slides from the segment you left to the one you chose.

React
tsx
import { PlSegment, PlSegmentedButton } from 'plass-ui';

<PlSegmentedButton aria-label="Period" value={period} onValueChange={setPeriod}>
  <PlSegment value="day">Day</PlSegment>
  <PlSegment value="week">Week</PlSegment>
</PlSegmentedButton>;
dart
import 'package:plass_ui/plass_ui.dart';

PlSegmentedButton<String>(
  semanticLabel: 'Period',
  value: period,
  onChanged: (String next) => setState(() => period = next),
  segments: const <PlSegment<String>>[
    PlSegment<String>(value: 'day', label: Text('Day')),
    PlSegment<String>(value: 'week', label: Text('Week')),
  ],
);

Props

PropTypeDefaultDescription
variantshared'solid' | 'glass' | 'ghost''glass'What the groove and the tile riding in it are made of. solid rides a tinted-glass key, glass a clear tile, ghost has no groove at all
sizeshared'xs' | 'sm' | 'md' | 'lg' | 'xl''md'Segment height and type scale — the same ladder as PlButton
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 of the groove. A groove is cut into the page, so the default is 0
valuestring | number | nullThe chosen segment. Use with onValueChange for a controlled set
defaultValuestring | number | nullnullWhich starts chosen
onValueChange(value: string | number | null) => voidCalled with the new value
fullWidthbooleanfalseThe segments share the full width, each taking an equal part of it
readOnlybooleanfalseShows which one is chosen but does not let it be changed
disabledbooleanfalseDisables every segment at once
namestringIdentifies the value when a form is submitted
childrenReactNodeThe PlSegment children
PropTypeDefaultDescription
segments * List<PlSegment<T>>The choices, as a list of descriptions rather than children — the set owns the roving focus, the arrow keys and the sliding tile
value * T?The chosen segment. Use with onValueChange for a controlled set
onChangedValueChanged<T>?Called with the new value
variantsharedPlassVariantPlassVariant.glassWhat the groove and the tile riding in it are made of. solid rides a tinted-glass key, glass a clear tile, ghost has no groove at all
sizesharedPlassSizePlassSize.mdSegment height and type scale — the same ladder as PlButton
colorsharedPlassColorPlassColor.primarySemantic colour role. Arbitrary colour values are not accepted
densitysharedPlassDensityPlassDensity.standardPadding only — never the height, never the type scale
elevationsharedint0Drop shadow depth of the groove. A groove is cut into the page, so the default is 0
fullWidthboolfalseThe segments share the full width, each taking an equal part of it
readOnlyboolfalseShows which one is chosen but does not let it be changed
disabledboolfalseDisables every segment at once
semanticLabelString?The name a screen reader gives the set. It has no visible label of its own

Every native <div> attribute passes straight through. color is excluded because it collides with the color in the table above, defaultValue and onChange because the set spells them defaultValue (a segment value) and onValueChange.

The set is generic in its segment's type (PlSegmentedButton<String>, PlSegmentedButton<Period>), so value and onChanged are typed rather than dynamic, and it is controlled, like every other control in the package.

PlSegment

PropTypeDefaultDescription
value * string | numberIdentifies the segment. What onValueChange reports
startIconReactNodeContent before the label. Sized in em, so it tracks the label
endIconReactNodeContent after the label — a count, a status dot
disabledbooleanfalseUnavailable, but still part of the set
childrenReactNodeThe segment's label
PropTypeDefaultDescription
value * TIdentifies the segment. What onValueChange reports
labelWidget?The segment's label
startIconWidget?Content before the label. Sized in em, so it tracks the label
endIconWidget?Content after the label — a count, a status dot
disabledboolfalseUnavailable, but still part of the set

variant, size and density are read from the PlSegmentedButton around the segment, not set on it. A segmented button whose third segment is a size out is not a segmented button.

A segment is a PlSegment, a description rather than a widget, for the reason a radio option is one: the set owns the roving focus, the arrow keys and the tile that slides between the segments, so it has to know which one is taken and where each one is.

It carries no variant, no size and no density, and could not. A segmented button whose third segment is a size out is not a segmented button.

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

Segmented button, tabs or select

  • Segmented button: a handful of short, mutually exclusive choices that filter what is already on screen: a period, a scope, a layout.
  • Tabs: the choice swaps whole panels of content.
  • Select: more than about five options, or long ones.

Examples

variant

The groove carries --plass-well, the one inset shadow in the library and the same one a solid field is drawn with. Those two are the whole of its use: a groove and a filled field are both a box something sits in. A slider's rail is not one, and no longer takes it. A rail is a line you look along.

solid puts the family's gradient in the tile with that family's tinted shadow under it, which is the design language's own sentence with nothing added: a key of tinted glass riding in a groove. glass and ghost lift a pane of clear glass instead and leave the label in the accent.

React

color

React

size

The same height ladder as PlButton, so a segmented button in a toolbar lines up with the buttons beside it.

React

fullWidth

The segments share the row and take an equal part of it each. The tile is re-measured after every layout, so it stays under its segment while the container changes width.

React

startIcon and endIcon

Both are sized against the label rather than against the row. An icon-only segment still needs a name of its own.

React

Accessibility

  • The set is a role="radiogroup" and each segment is a real radio, which is the whole accessibility argument: a segmented button is "exactly one of these". Built out of aria-pressed toggles it would announce four independent switches, three of which happen to be off.
  • One tab stop for the whole set; move within it. Base UI owns the roving tab index.
  • Give the set an aria-label. It has no visible label of its own, and a group with no name is a group a screen reader announces as "radio group".
  • Each segment is announced as one of a mutually exclusive set, taken or not. A segmented button is "exactly one of these". Built out of toggles it would announce four independent switches, three of which happen to be off.
  • One focus stop for the whole set: exactly one segment is in the tab order and the rest are wrapped in an ExcludeFocus. move the choice, wrapping at both ends.
  • A segment's focus ring turns inward, because a ring drawn outside one inside a groove would be painted over its neighbours.
  • Give the set a semanticLabel. It has no visible label of its own.

Differences from the React build

ReactFlutterWhy
<PlSegment> childrensegments, as descriptionsThe set owns the roving focus, the arrow keys and the sliding tile, so it has to know which one is taken and where each one is.
defaultValue / onValueChangevalue / onChangedFlutter's own controls are controlled, and its name for the callback.
a value of string | numbera generic TDart has generics, so the type is checked rather than restrained by convention.
four CSS custom properties on the tilea measured Rect and an AnimatedPositionedThe same idea (measure the chosen segment, animate the box) in Flutter's words. Nothing is transformed either way.
aria-labelsemanticLabelFlutter's name.
name, and a hidden inputThere is no native form submission to be part of.
  • The focus ring is drawn inset, because an offset ring on a segment inside a groove would be painted over its neighbours.
  • The tile animates left, top, width and height rather than a transform: it is an empty box, so no label is resampled while it travels. That is what lets the house no-transform rule survive a component whose entire point is that something moves.
  • The first choice of an empty set appears in place rather than flying in from the left edge. The tile is not mounted until there is something to sit under.

Released under the MIT License