ButtonGroup
A row of buttons that belong together. The corners that face a neighbour are squared off, and the shared props are set once for the whole set.
import { Button, ButtonGroup } from 'neba';
<ButtonGroup variant="outline">
<Button>Day</Button>
<Button>Week</Button>
<Button>Month</Button>
</ButtonGroup>;Props
| Prop | Type | Default | Description |
|---|---|---|---|
| variantshared | 'solid' | 'outline' | 'text' | — | Applied to every button in the group. Unset means the Button's own default stands |
| sizeshared | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | — | Height and type scale for every button in the group |
| colorshared | 'primary' | 'secondary' | 'success' | 'warning' | 'danger' | 'info' | — | Colour role for every button in the group. A button's own prop still wins |
| densityshared | 'default' | 'compact' | — | Horizontal padding for every button in the group |
| elevationshared | 0 | 1 | 2 | 3 | — | Drop shadow depth for every button in the group |
| orientationshared | 'horizontal' | 'vertical' | 'horizontal' | Which way the buttons run. Vertical flattens the top and bottom corners instead |
| disabled | boolean | — | Disables every button in the group at once |
| fullWidth | boolean | false | Stretches to the container and divides the width evenly between the buttons |
| children | ReactNode | — | The buttons. The shared props reach them even through a wrapper |
Every native <div> attribute passes straight through, minus color.
Examples
Shared props
Two things are happening here, and only one of them is visual. The corners are the look; the other half is that variant, size, color, density, elevation and disabled are stated once rather than repeated on every button. A group where one button is a size out is the failure this exists to prevent.
A button's own prop still wins — a row of secondary actions with one danger button in it is a real thing.
import { Button, ButtonGroup } from 'neba';
export default function ButtonGroupShared() {
return (
<div className="flex flex-col items-start gap-4">
{/* Set once for the whole group. */}
<ButtonGroup size="sm" variant="outline" color="secondary">
<Button>Copy</Button>
<Button>Duplicate</Button>
<Button>Archive</Button>
</ButtonGroup>
{/* A button can still override what the group said. */}
<ButtonGroup size="sm" variant="outline" color="secondary">
<Button>Copy</Button>
<Button>Duplicate</Button>
<Button color="danger">Delete</Button>
</ButtonGroup>
</div>
);
}Orientation
import { Button, ButtonGroup } from 'neba';
export default function ButtonGroupOrientation() {
return (
<div className="flex flex-wrap items-start gap-8">
<ButtonGroup orientation="horizontal" variant="outline">
<Button>Left</Button>
<Button>Centre</Button>
<Button>Right</Button>
</ButtonGroup>
<ButtonGroup orientation="vertical" variant="outline">
<Button>Top</Button>
<Button>Middle</Button>
<Button>Bottom</Button>
</ButtonGroup>
</div>
);
}Full width
import { Button, ButtonGroup } from 'neba';
export default function ButtonGroupFullWidth() {
return (
<div className="w-full max-w-md">
<ButtonGroup fullWidth variant="outline" color="secondary">
<Button>Cancel</Button>
<Button>Save draft</Button>
<Button>Publish</Button>
</ButtonGroup>
</div>
);
}How the seam works
Only the outline group pulls its buttons together by a pixel. Two hairline borders meeting would otherwise draw a seam twice as heavy as every other edge on the page, so the second button is shifted back to share a single line.
A solid group must not do that. Its seam is the plate edge — the white inset hairline every filled surface carries — and overlapping would put one button's fill over the neighbour's edge and merge the run into one blob.
Accessibility
- Renders
role="group". Give it anaria-labelwhen the buttons alone do not say what the set is for. - This is not a segmented control and it does not manage selection. For one-of-a-set, use a RadioGroup — that is what that actually is.
- The hovered or focused button is raised above its neighbours so its focus ring is never clipped.