Skip to main content
Urbicon UI

Button Group

Group related buttons with single/multi selection, orientation options, and connected styling.

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

View switcher

Single selection acts as a radio-group: bind:value (a ButtonGroupValue, declared as $state in the script) 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 in the script narrows the union). Icon-only buttons need an aria-label each; the group carries an ariaLabel for its purpose. tier=modify gives the soft caps of a toolbar surface.

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, guarding the range; clicking the value resets it.
<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 has a single slot, base — the group container itself. Stretch it and give every child equal width for form footers or mobile-friendly switchers; tailwind-merge resolves the conflict with the default inline-flex.
<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

unstyled drops the default single-row layout so you own it — here a wrapping chip row. Selection state, ARIA roles, and prop propagation to the child Buttons keep working. Setting connected to false lets each chip keep its own rounded caps.
<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

For a recurring off-palette look — here a floating map-overlay pill — register a ButtonGroup preset once at the app root and reference it by name. Presets keep hover, active, and dark-mode logic coherent instead of scattering class overrides.
<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

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';