Segmented
A single-select toggle bar: pick one of a few options from a connected button group.
Live demo
Segmented picks one option from a small, fixed set rendered as a connected button bar. It's the compact alternative to a radio group when the choices are few and worth showing at once.
It is a role="radiogroup" of role="radio" buttons with roving tabindex: the selected segment is the tab stop, arrow keys move and select with wraparound, and Home/End jump to the ends. Options are declared as a comma-separated options string (bare labels, or label:value pairs), or as a structured { value, label }[] when a label differs from its value or carries a comma or colon. For rich segments, drop in <Segment value="…" label="…"> children holding an icon or other markup instead of a text label; they win over options when both are present. It's form-associated; give it a name and the chosen value submits. Three sizes: sm, md, lg.
When to use
How this component composes with the rest of the set.
Props
9 props, straight from the manifest.
| Prop | Type | Default | Bindings | Description |
|---|---|---|---|---|
Appearance
Sizes
sm
Compact.
md
Default.
lg
Large.
States
selected
The chosen segment: filled with the accent and its readable foreground. When the theme's --selection-cue resolves to marker (a high-contrast or redundant-cues algorithm), the selected segment gains a non-color check glyph so selection never rests on color alone.
hover
Pointer over an unselected segment; an overlay paints the hover tint.
focus-visible
Keyboard focus on a segment: a token ring plus a transparent outline that becomes real in forced-colors mode.
disabled
The whole group disabled: muted, non-interactive.
Anatomy
The named parts that make up the component, with their selectors.
field
The wrapper stacking the optional label over the toggle bar.
segmented
The role="radiogroup" track holding the options.
option
Each role="radio" segment; the selected one fills with the accent.
label
The optional visible label, referenced as the group's accessible name.
badge
A per-option trailing count/status inside its segment; reach it at ::part(badge).
Tokens & coverage
What the component consumes, checked live against what the algorithm produces.
Live coverage check against the xtyle-default register
(derive(xtyleDefault, { anchors }) →
coverComponent(manifest, register)). Every token this component
consumes must be a key the algorithm produces.
--accent
--accent-2
--accent-2-fg
--accent-3
--accent-3-fg
--accent-4
--accent-4-fg
--accent-fg
--bg-1
--black
--black-fg
--blue
--blue-fg
--border-normal
--border-thick
--border-thin
--brown
--brown-fg
--cyan
--cyan-fg
--danger
--danger-fg
--duration-fast
--ease-standard
--fg-0
--fg-1
--fg-disabled
--font-sans
--gray
--gray-fg
--green
--green-fg
--info
--info-fg
--line-2
--neutral
--neutral-fg
--orange
--orange-fg
--pink
--pink-fg
--purple
--purple-fg
--radius-md
--radius-sm
--red
--red-fg
--ring
--selection-cue
--space-1
--space-2
--space-3
--space-4
--state-disabled
--state-hover
--state-press
--success
--success-fg
--text-body
--text-sm
--warn
--warn-fg
--weight-medium
--white
--white-fg
--yellow
--yellow-fg
Slots
Rich-content segments: one [slot="segment"] child per option, each carrying value, an accessible label, and markup like an <Icon>. The element derives selection, roving tabindex, and the form value from them exactly as it would from options, and projects each child's live content into its radio. Children win over options when both are given. In Astro and Svelte, use the <Segment> component rather than writing slot="segment" yourself: <Segment> carries the marker on its own root element, whereas a bare slot="segment" child of <Segmented> is a routing directive Astro consumes, and the content is discarded.
Accessibility
Code
Labels, value pairs, and sizes
A view switch, an alignment control using label:value pairs, the compact size, and a disabled group.
<xtyle-segmented label="View" options="Day,Week,Month" value="Week"></xtyle-segmented>
<xtyle-segmented label="Align" options="Left:start,Center:center,Right:end" value="center"></xtyle-segmented>
<xtyle-segmented size="sm" label="Theme" options="Light,Dark,Auto" value="Auto"></xtyle-segmented>
<xtyle-segmented label="Locked" options="One,Two,Three" value="Two" disabled></xtyle-segmented>
<script lang="ts">
import { Segmented } from "@xtyle/svelte";
let view = $state("Week");
</script>
<Segmented label="View" options="Day,Week,Month" bind:value={view} />
<Segmented label="Align" options="Left:start,Center:center,Right:end" value="center" />
<Segmented size="sm" label="Theme" options="Light,Dark,Auto" value="Auto" />
<Segmented label="Locked" options="One,Two,Three" value="Two" disabled />
---
import { Segmented } from "@xtyle/astro";
---
<Segmented label="View" options="Day,Week,Month" value="Week" />
<Segmented label="Align" options="Left:start,Center:center,Right:end" value="center" />
<Segmented size="sm" label="Theme" options="Light,Dark,Auto" value="Auto" />
<Segmented label="Locked" options="One,Two,Three" value="Two" disabled />
Structured options
An export-format row built from a { value, label }[], showing Markdown / HTML / EPUB while the handler receives md / html / epub.
<xtyle-segmented id="fmt" label="Export"></xtyle-segmented>
<script>
document.getElementById("fmt").options = [
{ value: "md", label: "Markdown" },
{ value: "html", label: "HTML" },
{ value: "epub", label: "EPUB" },
];
</script>
<script lang="ts">
import { Segmented } from "@xtyle/svelte";
const formats = [
{ value: "md", label: "Markdown" },
{ value: "html", label: "HTML" },
{ value: "epub", label: "EPUB" },
];
let format = $state("md");
</script>
<Segmented label="Export" options={formats} bind:value={format} />
---
import { Segmented } from "@xtyle/astro";
const formats = [
{ value: "md", label: "Markdown" },
{ value: "html", label: "HTML" },
{ value: "epub", label: "EPUB" },
];
---
<Segmented label="Export" options={formats} value="md" />
Per-option disabled and badges
A data-conditional view switch: a report segment carries a count while text / log stay disabled until their data exists, so the arrow keys skip them and the selection lands on the first enabled option.
<xtyle-segmented id="run-view" label="Run output"></xtyle-segmented>
<script>
document.getElementById("run-view").options = [
{ value: "report", label: "Report", badge: "128", title: "The parsed summary view" },
{ value: "text", label: "Text", disabled: true, title: "Raw text (no run yet)" },
{ value: "log", label: "Log", disabled: true, title: "Execution log (no run yet)" },
];
</script>
Icon segments
Drop <Segment> children holding an <Icon> in place of text labels; each carries a value and an accessible label, and the bar keeps its selection, roving focus, and form value unchanged. The children project live, so framework-owned content stays reactive.
<xtyle-segmented label="Severity" value="info">
<span slot="segment" value="info" label="Info"><xtyle-icon name="info"></xtyle-icon></span>
<span slot="segment" value="warn" label="Warnings"><xtyle-icon name="warning"></xtyle-icon></span>
<span slot="segment" value="error" label="Errors"><xtyle-icon name="error"></xtyle-icon></span>
</xtyle-segmented>
<script lang="ts">
import { Segmented, Segment, Icon } from "@xtyle/svelte";
let severity = $state("info");
</script>
<Segmented label="Severity" bind:value={severity}>
<Segment value="info" label="Info"><Icon name="info" /></Segment>
<Segment value="warn" label="Warnings"><Icon name="warning" /></Segment>
<Segment value="error" label="Errors"><Icon name="error" /></Segment>
</Segmented>
---
import { Segmented, Segment, Icon } from "@xtyle/astro";
---
<Segmented label="Severity" value="info">
<Segment value="info" label="Info"><Icon name="info" /></Segment>
<Segment value="warn" label="Warnings"><Icon name="warning" /></Segment>
<Segment value="error" label="Errors"><Icon name="error" /></Segment>
</Segmented>