Skip to main content
Urbicon UI
source

Segment Group

A compact control for switching between a few mutually exclusive views or modes.

Playground

Variant Style variant
Size Style variant
Tier Style variant
<SegmentGroup>
  <SegmentItem value="list">List</SegmentItem>
  <SegmentItem value="grid">Grid</SegmentItem>
  <SegmentItem value="board">Board</SegmentItem>
</SegmentGroup>

01 Purpose

Reach for a SegmentGroup when a handful of options are mutually exclusive and switching between them is the whole interaction: a view mode, a time range, a display density. It shows every option at once and slides the selection between them.

Each option is a SegmentItem with a value, and the group's value is whichever one is selected. bind:value keeps it in a variable, onValueChange gives you the new value for a side effect like refetching. Where the row runs out of width, collapseOnOverflow turns it into a vertical stack instead of letting it overflow, so every option stays visible.

ComponentReach for it when
SegmentGroup2–5 mutually exclusive views or modes, in one neutral style. The group hands you a value and you decide what to render with it.
ButtonGroup selection="single"You need button variants and intents, connected borders, or multi-select.
RadioGroupYou're collecting a value in a form: labels, descriptions, helper and error text.
TabEach option owns a panel that assistive technology should tie to it. Tab renders role="tablist" with aria-controls, where a SegmentGroup is a radiogroup that knows nothing about your markup.

02 Examples

View switcher

The common case: one dataset, a few mutually exclusive views.

Atlas

12 members

Nova

8 members

Orbit

5 members

<SegmentGroup bind:value={view} size="sm" ariaLabel="View mode">
  <SegmentItem value="list">List</SegmentItem>
  <SegmentItem value="cards">Cards</SegmentItem>
</SegmentGroup>

{#if view === 'list'}
  <ul
    class="border-border-subtle divide-border-subtle w-full max-w-md divide-y rounded-xl border"
  >
    {#each teams as team (team.id)}
      <li class="flex items-center justify-between px-4 py-3">
        <span class="text-text-primary text-sm font-medium">{team.name}</span>
        <span class="text-text-tertiary text-xs">{team.meta}</span>
      </li>
    {/each}
  </ul>
{:else}
  <div class="grid w-full max-w-md grid-cols-3 gap-3">
    {#each teams as team (team.id)}
      <Card variant="outlined" padding="sm" class="text-center">
        <p class="text-text-primary text-sm font-medium">{team.name}</p>
        <p class="text-text-tertiary mt-1 text-xs">{team.meta}</p>
      </Card>
    {/each}
  </div>
{/if}

Time-range selector

A compact range switch for a chart or dashboard header. bind:value keeps the current range, and onValueChange is where the refetch goes. Two to five options fit, past that reach for a Menu.

Showing revenue for the last week.

<SegmentGroup bind:value={range} onValueChange={loadRevenue} size="sm" ariaLabel="Time range">
  <SegmentItem value="1d">1D</SegmentItem>
  <SegmentItem value="1w">1W</SegmentItem>
  <SegmentItem value="1m">1M</SegmentItem>
  <SegmentItem value="1y">1Y</SegmentItem>
</SegmentGroup>
<p class="text-text-secondary text-sm">
  Showing revenue for the last
  <span class="text-text-primary font-medium">{loadedRange}</span>.
</p>

Inside a settings panel

mint=scale grows a segment slightly while the pointer rests on it, which suits a control that sits quietly in a settings row until someone reaches for it.
Appearance
<div
  class="border-border-subtle bg-surface-elevated flex w-full max-w-sm items-center justify-between rounded-2xl border p-4"
>
  <span class="text-text-primary text-sm font-medium">Appearance</span>
  <SegmentGroup bind:value={theme} size="sm" mint="scale" ariaLabel="Theme preference">
    <SegmentItem value="light">Light</SegmentItem>
    <SegmentItem value="dark">Dark</SegmentItem>
    <SegmentItem value="system">System</SegmentItem>
  </SegmentGroup>
</div>

03 Customization

Primary-tinted control

The track and the sliding indicator take the primary intent tokens through the group's slotClasses; the label rides item, which belongs to each SegmentItem. Note the two text roles: primary-text is the AA-rated step for a label on paper, text-on-primary the one that reads on the indicator's fill. Radius tier, padding, shadow and the slide animation stay, and because the look rides the intent palette it re-themes with the rest of the app.
<SegmentGroup
  bind:value={plan}
  ariaLabel="Billing plan"
  slotClasses={{
    base: 'bg-primary-subtle',
    indicator: 'bg-primary'
  }}
>
  <SegmentItem value="monthly" slotClasses={tintedLabel}>Monthly</SegmentItem>
  <SegmentItem value="yearly" slotClasses={tintedLabel}>Yearly</SegmentItem>
</SegmentGroup>

This is one of five ways to restyle a block. See Customization for class, slotClasses, unstyled, preset and provider-level overrides.

04 Accessibility

Built-in ARIA

The container is a role="radiogroup" and each segment a role="radio" carrying aria-checked, so the active option is announced as a selected radio. The sliding indicator is aria-hidden, so it is never announced. Pass ariaLabel to name the group's purpose.

Keyboard

Arrow keys move between segments and select as they go. Home and End do the same for the first and last, so both of them change the value. Only the active segment is in the tab order (roving tabindex), so Tab enters and leaves the group as a single stop.

Reduced motion

Under prefers-reduced-motion the indicator moves to its new segment without the slide, and a mint preset plays nothing at all.

05 API Reference

17 props
17 props
Prop
Type
Default
Description

06 Types

Local type definitions used by this component.

11 types
Name
Kind
Category
Used by
Description

07 Installation

Import

import { SegmentGroup, SegmentItem } from '@urbicon-ui/blocks';