Button Group
Group related buttons with single/multi selection, orientation options, and connected styling.
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
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>02 Customization
Full-width group
<ButtonGroup
selection="single"
value="all"
slotClasses={{ base: 'flex w-full [&>*]:flex-1' }}
ariaLabel="Filter scope"
>
<Button value="all">All</Button>
<Button value="active">Active</Button>
<Button value="archived">Archived</Button>
</ButtonGroup>Unstyled: wrapping filter chips
<ButtonGroup
unstyled
selection="multiple"
connected={false}
size="sm"
class="flex max-w-sm flex-wrap gap-2"
ariaLabel="Amenity filters"
>
<Button value="balcony">Balcony</Button>
<Button value="garden">Garden</Button>
<Button value="parking">Parking</Button>
<Button value="elevator">Elevator</Button>
<Button value="furnished">Furnished</Button>
<Button value="pets">Pets allowed</Button>
</ButtonGroup>Reusable preset via BlocksProvider
<BlocksProvider
presets={{
ButtonGroup: {
floating: {
slotClasses: {
base: 'bg-surface-overlay border-border-subtle rounded-full border p-1 shadow-[var(--blocks-shadow-md)]'
}
}
}
}}
>
<ButtonGroup
preset="floating"
connected={false}
variant="ghost"
size="sm"
selection="single"
value="standard"
ariaLabel="Map style"
>
<Button value="standard">Standard</Button>
<Button value="satellite">Satellite</Button>
</ButtonGroup>
</BlocksProvider>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
Tab moves focus between buttons. Enter / Space toggles selection.
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
Prop | 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. | |
onSelectionChange | (value: ButtonGroupValue, selectedValues: string[]) => void | — | Fired when selection changes. Receives the new value and an array of all selected values. | |
orientation | ButtonGroupOrientation | — | Orientation property for the ButtonGroup component | |
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 (default) →
pill caps for the group; modify → soft caps. Inherits from a wrapping
Toolbar via TierContext when not set explicitly. | |
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';