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
10 props, straight from the manifest.
| Prop | Type | Default | Bindings | Description |
|---|---|---|---|---|
false to keep the control out of sequential focus navigation, for an app driving selection from its own keyboard cursor. The host's own tabindex cannot express this, because focus lands on the inner control rather than the host, and setting it there from outside does not survive the next render. | ||||
Day,Week, the label is the value) or label:value pairs (Left:start). For labels that differ from their value or carry a comma or colon, pass a { value, label }[] instead (the JS property in html / svelte, a JSON array attribute in Astro); a bare string[] works too. The structured form also takes a per-option disabled (a choice the current data can't offer, skipped by pointer and keyboard), a badge (trailing text like a count), and a title (a hover hint on the segment, for a short label that needs disambiguating context) per segment. | ||||
sm, md, or lg. | ||||
aria-labelledby. | ||||
label. | ||||
labelledby and a visible label both win over it. | ||||
Events
What the component emits, and what rides along on event.detail. The name in the
first column is the one addEventListener takes.
| Event | Detail | 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. A ::part() handle is reachable from your own stylesheet; the class beside it is the component's internal selector, which a shadow boundary keeps to itself.
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-fgSlots
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>