Button Group
Group related buttons with single or multiple selection.
Playground
<ButtonGroup
intent="neutral"
selection="single"
size="md"
variant="outlined"
>
<Button value="left">Left</Button>
<Button value="center">Center</Button>
<Button value="right">Right</Button>
</ButtonGroup>01 Examples
A ButtonGroup joins related buttons into one control with shared borders, connected by default and spaced out when you set connected to false. selection sets the behaviour: single is a radio group, multiple a checkbox group, and none (the default) a plain row of actions. Set size, intent, variant or mint once on the group and every button inherits it.
View switcher
<div class="flex w-full flex-col gap-4">
<ButtonGroup selection="single" bind:value={view} size="sm" ariaLabel="Listing view">
<Button value="list"><ListIcon size={16} />List</Button>
<Button value="gallery"><GalleryIcon size={16} />Gallery</Button>
<Button value="map"><MapIcon size={16} />Map</Button>
</ButtonGroup>
{#if view === 'list'}
<div class="flex flex-col gap-2" aria-hidden="true">
{#each ['row-1', 'row-2', 'row-3'] as row (row)}
<div class="bg-surface-elevated border-border-subtle h-9 rounded-lg border"></div>
{/each}
</div>
{:else if view === 'gallery'}
<div class="grid grid-cols-3 gap-2" aria-hidden="true">
{#each ['tile-1', 'tile-2', 'tile-3', 'tile-4', 'tile-5', 'tile-6'] as tile (tile)}
<div
class="bg-surface-elevated border-border-subtle aspect-video rounded-lg border"
></div>
{/each}
</div>
{:else if view === 'map'}
<div
class="bg-surface-elevated border-border-subtle text-text-tertiary flex h-28 items-center justify-center rounded-lg border"
>
<MapPinIcon size={24} />
</div>
{:else}
<p class="text-text-tertiary text-sm">No view selected.</p>
{/if}
</div>Formatting toggles
Bright three-room flat with south-facing balcony, five minutes from the station.
<ButtonGroup
selection="multiple"
bind:value={formats}
size="sm"
tier="modify"
ariaLabel="Text formatting"
>
<Button value="bold" aria-label="Bold"><BoldIcon size={16} /></Button>
<Button value="italic" aria-label="Italic"><ItalicIcon size={16} /></Button>
<Button value="underline" aria-label="Underline"><UnderlineIcon size={16} /></Button>
</ButtonGroup>
<p
class={[
'text-text-primary text-sm',
hasFormat('bold') && 'font-bold',
hasFormat('italic') && 'italic',
hasFormat('underline') && 'underline'
]}
>
Bright three-room flat with south-facing balcony, five minutes from the station.
</p>Zoom control
<ButtonGroup ariaLabel="Zoom">
<Button aria-label="Zoom out" disabled={zoom <= 25} onclick={() => (zoom -= 25)}>
<ZoomOutIcon size={16} />
</Button>
<Button class="min-w-20 tabular-nums" onclick={() => (zoom = 100)}>{zoom}%</Button>
<Button aria-label="Zoom in" disabled={zoom >= 200} onclick={() => (zoom += 25)}>
<ZoomInIcon size={16} />
</Button>
</ButtonGroup>Text alignment
Sunlit corner studio with a reading nook, one block from the park and the market.
<ButtonGroup
selection="single"
bind:value={align}
orientation="vertical"
size="sm"
ariaLabel="Text alignment"
>
<Button value="left"><AlignLeftIcon size={16} />Left</Button>
<Button value="center"><AlignCenterIcon size={16} />Center</Button>
<Button value="right"><AlignRightIcon size={16} />Right</Button>
</ButtonGroup>
<p class={['text-text-primary max-w-xs text-sm leading-relaxed', alignClass]}>
Sunlit corner studio with a reading nook, one block from the park and the market.
</p>02 Customization
Floating glass controls
slotClasses tints the group into a translucent control cluster for a map overlay. It keeps the pill radius tier, size and behaviour, and the raw colours cover only the glass fill, border, blur and the white icons. Glass has no token equivalent.<BlocksProvider
defaults={{
ButtonGroup: {
slotClasses: {
base: 'rounded-commit border border-white/20 bg-white/10 p-1 shadow-[var(--blocks-shadow-lg)] backdrop-blur-xl'
}
},
Button: {
slotClasses: {
base: 'text-white hover:bg-white/20'
}
}
}}
>
<ButtonGroup ariaLabel="Map controls" connected={false} variant="ghost" size="sm">
<Button aria-label="Zoom in">
<ZoomInIcon size={16} />
</Button>
<Button aria-label="Zoom out">
<ZoomOutIcon size={16} />
</Button>
<Button aria-label="Recenter">
<MapPinIcon size={16} />
</Button>
</ButtonGroup>
</BlocksProvider>This is one of five ways to restyle a block. See Customization for class, slotClasses, unstyled, preset and provider-level overrides.
03 Accessibility
ARIA
Single-selection groups use role="radiogroup" with role="radio" + aria-checked on each button. Multiple-selection
groups use role="group" with role="checkbox" + aria-checked. Provide ariaLabel when the group's purpose is not clear from
context, and an aria-label on every icon-only child Button.
Keyboard
Single-selection groups are a radiogroup: Tab moves focus into the group (to the selected segment, or the first when none
is selected), and ArrowLeft / ArrowRight (or ArrowUp / ArrowDown), Home and End move between segments and
change the selection. Multiple-selection and plain (selection="none") groups place every button in the tab order, so Tab moves between them. Enter / Space activates the focused button.
Prop Inheritance
size, intent, variant, and mint propagate to child Buttons via context, and the
group wins: the same prop set on an individual Button inside a group is ignored, so
configure these once on the group. disabled combines: a disabled group disables every child, and a child can additionally disable itself.
Only tier can be overridden per Button.
04 API Reference
19 propsProp | Type | Default | Description | |
|---|---|---|---|---|
ariaLabel | string | — | Accessible label for the group (prefer this over aria-label for correct HTML attribute). | |
ariaLabelledBy | string | — | ID of the element that labels the group. | |
children | Snippet | — | Button children to group. | |
class | string | — | Extra classes merged onto the root element. | |
connected | boolean | — | Visually connect buttons (overlapping borders, shared rounding). When false, buttons are spaced with a small gap. | |
disabled | boolean | — | Disable the entire group and all child Buttons. | |
intent | ComponentIntent | — | Semantic colour propagated to child Buttons. | |
mint | MintProp | 'none' | Micro-interaction preset applied to each child Button (per-item via
context); overrides each button's own mint prop.
The 'none' default also flattens each button's press sink, so a connected
group's shared seam stays still on click instead of one segment shrinking
away from its neighbours. Buttons keep reporting the press in depth and
colour. Name any real mint here to give the whole group its movement back. | |
onSelectionChange | (value: ButtonGroupValue, selectedValues: string[]) => void | — | Fired when selection changes. Receives the new value and an array of all selected values. | |
orientation | ButtonGroupOrientation | — | Stack direction. | |
preset | string | — | Apply a named preset registered via <BlocksProvider presets={{ ButtonGroup: {...} }}>.
Prefer this over class overrides when the requested look falls outside the
semantic intent palette — presets keep hover/active/dark-mode logic coherent
and make the custom look reusable across the project. | |
selection | ButtonGroupSelection | — | Selection mode. "single" = radio-group, "multiple" = checkbox-group, "none" = no selection. | |
size | ComponentSize | — | Size propagated to child Buttons (the group value wins over a Button's own size). | |
slotClasses | Partial<Record<ButtonGroupSlots, string>> | — | Per-slot class overrides. Slots: base | |
tier | InteractiveTier | — | Semantic radius tier propagated to child Buttons. commit → pill caps for
the group; modify → soft caps. Inherits from a wrapping Toolbar via
TierContext when not set explicitly. The unset default is commit, except
on a connected vertical group, where the pill cap domes the stack into a
lozenge — that one defaults to modify. Set tier="commit" explicitly to
get the capsule back (right for a narrow, icon-only stack). | |
unstyled | boolean | — | Remove all default tv classes. | |
value | ButtonGroupValue | — | Current selection value. Bind with bind:value for two-way sync. String for single, string[] for multiple. | |
variant | ButtonVariants['variant'] | — | Visual weight propagated to child Buttons. | |
...HTMLAttributes<HTMLDivElement> inherited | HTMLAttributes | — | HTML attributes (excluding: 'children') |
05 Types
Local type definitions used by this component.
Name | Kind | Category | Used by | Description | |
|---|---|---|---|---|---|
ButtonGroupSelection | type | helper | 1 | — | |
ButtonGroupOrientation | type | helper | 1 | — | |
ButtonGroupValue | type | helper | 1 | — | |
ButtonGroupProps | interface | props | 0 | — | |
ButtonGroupContext | interface | helper | 0 | Reactive context exposed to child Button components via getButtonGroupContext(). | |
ButtonGroupVariants | type | variant | 0 | — | |
ButtonGroupSlots | type | variant | 0 | Slot names derived from the tv() config — single source of truth for slotClasses. | |
MintProp | type | helper | 1 | — | |
ComponentIntent | type | helper | 1 | — | |
ComponentSize | type | helper | 1 | Standard size scale for components.
Most components support a subset of this scale:
- **Compact** (3 sizes): sm | md | lg – Menu, Pagination, Popover, Tab, Tooltip
- **Standard** (5 sizes): xs | sm | md | lg | xl – Input, Spinner, ButtonGroup
- **Extended** (4 sizes): xs | sm | md | lg – Badge, Checkbox, Toggle
Button and Avatar extend this scale with 2xs / 2xl respectively. | |
ComponentVariant | type | variant | 0 | — | |
InteractiveTier | type | helper | 1 | Semantic radius tier for interactive surfaces (3-tier system).
- commit → r-human (CTA, identity, status declarations)
- modify → r-interactive (fields, navigation, secondary actions)
Container components (Card, Alert, Toolbar surface, …) live in a third
tier contain (r-structure) which is **not** part of this propagation
context — those surfaces are always r-structure by design and have no
tier-flip use case. | |
ButtonVariants | type | variant | 0 | — | |
MintName | type | helper | 0 | A mint name: a built-in (autocompleted), 'none' to disable, or any
consumer-registered name. (string & {}) keeps the registry open — a
custom name still type-checks, it just isn't suggested. A typo therefore
also still compiles (it resolves like an unregistered custom name and
warns at runtime); the union buys completion and docs, not validation. | |
MintConfig | interface | helper | 0 | — | |
VariantProps | type | helper | 0 | — | |
BuiltinMintName | type | helper | 0 | Built-in mint names as a literal union, so the mint prop autocompletes
across every component — the single list the hand-curated playground knobs
and docs used to drift away from. |
06 Installation
Import
import { ButtonGroup, Button } from '@urbicon-ui/blocks';