Skip to main content
Urbicon UI

Button Group

Group related buttons with single or multiple selection.

Playground

Orientation
Tier
Selection
Variant
<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

Single selection acts as a radio-group: bind:value holds the current ButtonGroupValue and drives which panel renders. Clicking the active segment again clears the selection, so handle the undefined case.
<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

Multiple selection acts as a checkbox-group: value holds a string[] of the pressed toggles (the hasFormat helper narrows the ButtonGroupValue union before reading it). Icon-only buttons each need an aria-label, and the group carries an ariaLabel for its purpose. tier=modify softens the group's corner rounding from the default pill caps, which suits a toolbar.

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

With selection=none (the default) the group is purely visual: three plain Buttons share size, intent, and connected borders while onclick does the work. Per-button disabled still applies on top of the group to guard the range, and the middle button resets the zoom.
<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

A vertical stack for a narrow inspector or side panel: orientation=vertical runs the segments top to bottom, and a connected vertical group softens its own caps — the pill radius that shapes the horizontal segmented control would dome a stack of text buttons into a capsule. Pass tier=commit to ask for that capsule anyway; it suits a narrow icon-only stack. Each button pairs an icon with a text label, so none needs an aria-label.

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

One 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 props
19 props
Prop
Type
Default
Description

05 Types

Local type definitions used by this component.

17 types
Name
Kind
Category
Used by
Description

06 Installation

Import

import { ButtonGroup, Button } from '@urbicon-ui/blocks';