Segment Group
A compact control for switching between a few mutually exclusive views or modes.
Playground
<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.
| Component | Reach for it when |
|---|---|
SegmentGroup | 2–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. |
RadioGroup | You're collecting a value in a form: labels, descriptions, helper and error text. |
Tab | Each 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
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
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.<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
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 propsProp | Type | Default | Description | |
|---|---|---|---|---|
ariaLabel | string | — | Accessible label for the segment group. | |
children | Snippet | — | Segment items to render. Must be SegmentItem components. | |
class | string | — | Extra classes merged onto the root element. | |
collapseOnOverflow | boolean | true | When the segments can't fit their available width, collapse the horizontal
track to a vertical radio-style stack (all options stay visible) instead of
overflowing. Triggered by real measured overflow (ResizeObserver), not a
viewport breakpoint, so it only engages when an instance genuinely doesn't
fit — a 2-segment switcher that fits stays horizontal. Set false to keep
the track horizontal (it still won't push the page wider than its parent). | |
disabled | boolean | false | Prevent interaction and dim the control. | |
fullWidth variant | true | false | Controls the fullWidth behavior and appearance of the SegmentGroup component. Available options: true. | |
mint | MintProp | 'none' | Micro-interaction preset applied to each segment item (per-item via context). Only applies while the item is not disabled. Accepts a preset name, an array of names, or configured mint objects. | |
onValueChange | (value: string) => void | — | Fires after the selected value changes. Receives the new value. | |
preset | string | — | Apply a named preset registered via <BlocksProvider presets={{ SegmentGroup: {...} }}>.
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. | |
size variant | lgmdsm | md | Controls the dimensions, padding, and text size of the SegmentGroup. Affects the component's physical footprint. Available options: lg, md, sm. | |
slotClasses | Partial<Record<Exclude<SegmentGroupSlots, 'item'>, string>> | — | Per-slot class overrides merged with tv() styles.
item is deliberately absent: the group renders only the track and the
indicator, so an item entry here would type-check and then do nothing.
It belongs on each SegmentItem, which owns that slot. | |
tier variant | commitmodify | commit | Selects the semantic radius tier of the SegmentGroup — the shape family it belongs to (--radius-commit/-modify/-contain/-bridge). Shape is retuned per family in your theme, so this picks the family rather than a pixel value. Available options: commit, modify. | |
unstyled | boolean | — | Remove all default tv() classes. | |
value | string | — | Currently selected value. Supports bind:value for two-way binding. | |
variant variant | defaulttext | default | Controls the visual style and presentation of the SegmentGroup. Determines the component's visual treatment. Available options: default, text. | |
...HTMLAttributes<HTMLDivElement> inherited | HTMLAttributes | — | HTML attributes (excluding: 'children') | |
...SegmentGroupVariants variant | VariantProps | — | Styling variants from SegmentGroupVariants |
06 Types
Local type definitions used by this component.
Name | Kind | Category | Used by | Description | |
|---|---|---|---|---|---|
RegisteredSegment | interface | helper | 0 | One registered segment, as the group tracks it.
isDisabled is a getter rather than a boolean so the group reads the item's
live state instead of a snapshot: the item derives it from its own disabled
prop OR the group's, and both can change after registration. | |
SegmentGroupContext | interface | helper | 0 | Reactive context exposed to child SegmentItem components. | |
SegmentGroupProps | interface | props | 0 | — | |
SegmentItemProps | interface | props | 0 | Individual option inside a SegmentGroup. | |
SegmentGroupVariants | type | variant | 1 | — | |
SegmentGroupSlots | type | variant | 0 | Slot names derived from the tv() config above — single source of truth for slotClasses. | |
MintProp | type | helper | 1 | — | |
InteractiveTier | type | helper | 0 | 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. | |
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 | — | |
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. |
07 Installation
Import
import { SegmentGroup, SegmentItem } from '@urbicon-ui/blocks';