Skip to content

PlButtonGroup

A run of buttons that belong together. The corners that face a neighbour are squared off, and variant, size, color, density, elevation and disabled are stated once for the set.

React
tsx
import { PlButton, PlButtonGroup } from 'plass-ui';

<PlButtonGroup variant="glass" color="secondary">
  <PlButton>Day</PlButton>
  <PlButton>Week</PlButton>
  <PlButton>Month</PlButton>
</PlButtonGroup>;
dart
import 'package:plass_ui/plass_ui.dart';

PlButtonGroup(
  variant: PlassVariant.glass,
  color: PlassColor.secondary,
  children: <Widget>[
    PlButton(onPressed: showDay, child: const Text('Day')),
    PlButton(onPressed: showWeek, child: const Text('Week')),
    PlButton(onPressed: showMonth, child: const Text('Month')),
  ],
);

Props

PropTypeDefaultDescription
variantshared'solid' | 'glass' | 'ghost'The material of the whole run. Unset, each button keeps its own default (solid)
sizeshared'xs' | 'sm' | 'md' | 'lg' | 'xl'Height and type scale for the whole run. A group with one button a size out is the failure this prevents
colorshared'primary' | 'secondary' | 'success' | 'warning' | 'danger' | 'info'Semantic colour role for the run. A button's own color still wins
densityshared'default' | 'compact'Horizontal padding for the run
elevationshared0 | 1 | 2 | 3Drop shadow depth for the run
orientationshared'horizontal' | 'vertical''horizontal'Which way the buttons run. vertical is a stacked menu of equal actions
disabledbooleanDisables every button in the group at once
fullWidthbooleanfalseStretches to the container and divides the width evenly between buttons
childrenReactNodeThe buttons. They stay real PlButtons
PropTypeDefaultDescription
children * List<Widget>The buttons, in order. A list rather than one child, because the group has to know which member is at each end to decide which corners to square
variantsharedPlassVariant?The material of the whole run. Unset, each button keeps its own default (solid)
sizesharedPlassSize?Height and type scale for the whole run. A group with one button a size out is the failure this prevents
colorsharedPlassColor?Semantic colour role for the run. A button's own color still wins
densitysharedPlassDensity?Horizontal padding for the run
elevationsharedint?Drop shadow depth for the run
orientationsharedPlassOrientationPlassOrientation.horizontalWhich way the buttons run. vertical is a stacked menu of equal actions
disabledbool?Disables every button in the group at once
fullWidthboolfalseStretches to the container and divides the width evenly between buttons

Every native <div> attribute passes straight through. color is excluded because it collides with the color in the table above.

children is a list rather than one child, and not only because that is Flutter's usual shape: the group has to know which member is at each end to decide which corners to square, and a widget handed one opaque subtree could not.

The axes are nullable on PlButton and PlIconButton too (PlassVariant?, PlassSize?, int?), because Dart has no way to tell a default apart from a value that was passed. null there means this button did not say, which is what leaves the run free to answer.

The five style axes have no default of their own: an axis the group does not state is one each button falls back to its own default on, so a group with no props changes nothing except the corners. A button that states an axis itself still wins. A run of secondary actions with one danger button in it is a real thing.

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

PlButtonGroup or PlSegmentedButton

The buttons stay real PlButtons and nothing about them is replaced: the group squares four corners and hands down six props. It does not manage selection, it has no value, and none of its buttons is ever the chosen one.

For one-of-a-set (a view switcher, a mode toggle) use PlSegmentedButton, which is that control and carries the roving focus and the radiogroup semantics that go with it.

Examples

variant

glass is the one variant with a seam to handle. It is also the only one that draws an edge, and two glass keys meeting would otherwise show both of their hairlines, twice the weight of every other edge on the page, so the second is pulled back a pixel and the two share one line.

solid must not do that. Its keys have no border to double up, and overlapping would put one gradient over the start of the next.

React

size

Stated once, so it cannot be a size out on one button. The heights are the library's control ladder, unchanged.

React

orientation

vertical stacks the run and squares the top and bottom edges instead of the sides. It is for a stacked menu of equal actions; horizontal is the default because that is what a toolbar is.

React

fullWidth

Stretches the group to its container and divides the width evenly between the buttons, so three actions across the bottom of a card are three equal thirds rather than three different lengths of word.

React

Accessibility

  • The group is a role="group". Give it an aria-label when the run needs a name of its own. A bar with three of these in it is three unnamed groups otherwise.
  • It is not a role="toolbar" and takes no roving focus. That role is a promise about keyboard behaviour, and every button here is its own tab stop, which is what ordinary <button> semantics already say.
  • The corners are squared with logical properties, so under RTL the first button is on the right and the flattened side follows it.
  • Each button gets a stacking context, so a focus ring (drawn outside the border box) is never painted over by the neighbour that comes after it.
  • disabled on the group disables every button in it; a button that sets disabled itself still wins.

Differences from the React build

ReactFlutterWhy
arbitrary childrenchildren: List<Widget>The group has to know which member is at each end to square the right corners.
a glass key is pulled back a pixel so two hairlines overlapthe key facing a neighbour does not draw that side at allFlutter has no negative margin, EdgeInsets asserts it is non-negative, and the alternative is a Transform, which this library does not put on a control. Both arrive at one hairline per seam.
an axis is left offthe same parameters are nullableDart cannot tell a default apart from a value that was passed, so not stated has to be a value the type can hold.
className, style, native attributesThere is no class list and no style attribute to pass through.

Released under the MIT License