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.
import { PlButton, PlButtonGroup } from 'plass-ui';
<PlButtonGroup variant="glass" color="secondary">
<PlButton>Day</PlButton>
<PlButton>Week</PlButton>
<PlButton>Month</PlButton>
</PlButtonGroup>;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
| Prop | Type | Default | Description |
|---|---|---|---|
| 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 |
| elevationshared | 0 | 1 | 2 | 3 | — | Drop shadow depth for the run |
| orientationshared | 'horizontal' | 'vertical' | 'horizontal' | Which way the buttons run. vertical is a stacked menu of equal actions |
| disabled | boolean | — | Disables every button in the group at once |
| fullWidth | boolean | false | Stretches to the container and divides the width evenly between buttons |
| children | ReactNode | — | The buttons. They stay real PlButtons |
| Prop | Type | Default | Description |
|---|---|---|---|
| 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 |
| variantshared | PlassVariant? | — | The material of the whole run. Unset, each button keeps its own default (solid) |
| sizeshared | PlassSize? | — | Height and type scale for the whole run. A group with one button a size out is the failure this prevents |
| colorshared | PlassColor? | — | Semantic colour role for the run. A button's own color still wins |
| densityshared | PlassDensity? | — | Horizontal padding for the run |
| elevationshared | int? | — | Drop shadow depth for the run |
| orientationshared | PlassOrientation | PlassOrientation.horizontal | Which way the buttons run. vertical is a stacked menu of equal actions |
| disabled | bool? | — | Disables every button in the group at once |
| fullWidth | bool | false | Stretches 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.
import { PlButton, PlButtonGroup } from 'plass-ui';
export default function ButtonGroupVariants() {
return (
<div className="flex flex-wrap items-center gap-4">
<PlButtonGroup variant="solid">
<PlButton>Cut</PlButton>
<PlButton>Copy</PlButton>
<PlButton>Paste</PlButton>
</PlButtonGroup>
<PlButtonGroup variant="glass" color="secondary">
<PlButton>Cut</PlButton>
<PlButton>Copy</PlButton>
<PlButton>Paste</PlButton>
</PlButtonGroup>
<PlButtonGroup variant="ghost" color="secondary">
<PlButton>Cut</PlButton>
<PlButton>Copy</PlButton>
<PlButton>Paste</PlButton>
</PlButtonGroup>
</div>
);
}import 'package:flutter/widgets.dart';
import 'package:plass_ui/plass_ui.dart';
class ButtonGroupVariants extends StatelessWidget {
const ButtonGroupVariants({super.key});
@override
Widget build(BuildContext context) {
return Wrap(
spacing: 16,
runSpacing: 16,
children: <Widget>[
for (final PlassVariant variant in PlassVariant.values)
PlButtonGroup(
variant: variant,
color: variant == PlassVariant.solid ? PlassColor.primary : PlassColor.secondary,
children: <Widget>[
PlButton(onPressed: () {}, child: const Text('Cut')),
PlButton(onPressed: () {}, child: const Text('Copy')),
PlButton(onPressed: () {}, child: const Text('Paste')),
],
),
],
);
}
}size
Stated once, so it cannot be a size out on one button. The heights are the library's control ladder, unchanged.
import { PlButtonGroup, PlButton } from 'plass-ui';
const sizes = ['xs', 'sm', 'md', 'lg', 'xl'] as const;
export default function ButtonGroupSizes() {
return (
<div className="flex flex-col items-start gap-3">
{sizes.map((size) => (
<PlButtonGroup key={size} size={size} variant="glass" color="secondary">
<PlButton>Back</PlButton>
<PlButton>Forward</PlButton>
</PlButtonGroup>
))}
</div>
);
}import 'package:flutter/widgets.dart';
import 'package:plass_ui/plass_ui.dart';
class ButtonGroupSizes extends StatelessWidget {
const ButtonGroupSizes({super.key});
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
spacing: 12,
children: <Widget>[
for (final PlassSize size in PlassSize.values)
PlButtonGroup(
size: size,
variant: PlassVariant.glass,
color: PlassColor.secondary,
children: <Widget>[
PlButton(onPressed: () {}, child: const Text('Back')),
PlButton(onPressed: () {}, child: const Text('Forward')),
],
),
],
);
}
}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.
import { PlButton, PlButtonGroup } from 'plass-ui';
export default function ButtonGroupOrientation() {
return (
<PlButtonGroup orientation="vertical" variant="glass" color="secondary">
<PlButton>Rename</PlButton>
<PlButton>Duplicate</PlButton>
<PlButton color="danger">Delete</PlButton>
</PlButtonGroup>
);
}import 'package:flutter/widgets.dart';
import 'package:plass_ui/plass_ui.dart';
class ButtonGroupOrientation extends StatelessWidget {
const ButtonGroupOrientation({super.key});
@override
Widget build(BuildContext context) {
return PlButtonGroup(
orientation: PlassOrientation.vertical,
variant: PlassVariant.glass,
color: PlassColor.secondary,
children: <Widget>[
PlButton(onPressed: () {}, child: const Text('Rename')),
PlButton(onPressed: () {}, child: const Text('Duplicate')),
PlButton(color: PlassColor.danger, onPressed: () {}, child: const Text('Delete')),
],
);
}
}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.
import { PlButton, PlButtonGroup } from 'plass-ui';
export default function ButtonGroupFullWidth() {
return (
<PlButtonGroup fullWidth variant="glass" color="secondary" className="max-w-sm">
<PlButton>Deny</PlButton>
<PlButton>Ask</PlButton>
<PlButton>Allow</PlButton>
</PlButtonGroup>
);
}import 'package:flutter/widgets.dart';
import 'package:plass_ui/plass_ui.dart';
class ButtonGroupFullWidth extends StatelessWidget {
const ButtonGroupFullWidth({super.key});
@override
Widget build(BuildContext context) {
return SizedBox(
width: 360,
child: PlButtonGroup(
fullWidth: true,
variant: PlassVariant.glass,
color: PlassColor.secondary,
children: <Widget>[
PlButton(onPressed: () {}, child: const Text('Deny')),
PlButton(onPressed: () {}, child: const Text('Ask')),
PlButton(onPressed: () {}, child: const Text('Allow')),
],
),
);
}
}Accessibility
- The group is a
role="group". Give it anaria-labelwhen 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.
disabledon the group disables every button in it; a button that setsdisableditself still wins.
Differences from the React build
| React | Flutter | Why |
|---|---|---|
arbitrary children | children: 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 overlap | the key facing a neighbour does not draw that side at all | Flutter 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 off | the same parameters are nullable | Dart 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 attributes | — | There is no class list and no style attribute to pass through. |