sveltelean Primitives
Versionv0.2.0 GitHub

Example

A view switch
View
<fieldset data-slean="segmented">
	<legend>View</legend>
	<label><input type="radio" name="segmented-view" value="day" /> Day</label>
	<label><input type="radio" name="segmented-view" value="week" checked /> Week</label>
	<label><input type="radio" name="segmented-view" value="month" /> Month</label>
	<label><input type="radio" name="segmented-view" value="year" /> Year</label>
</fieldset>
Tier 0: a click on a segment checks its radio; the arrow keys move the choice and wrap. Everything here works with page JavaScript disabled.

Why this implementation exists

A segmented control is a radio group with another look: one choice among a few, one tab stop, the arrow keys to move, a value in the form. Radios sharing a name do all of that natively, with the group named by its <legend> and each segment named by its label.

The stylesheet draws the labels as equal-width segments of one bar. Each radio covers its segment, transparent and focusable, so a click anywhere on the segment checks it; label:has(input:checked) raises the checked segment and label:has(input:focus-visible) draws the keyboard ring on it. There is no element to keep in sync, because the checked state is the radio’s.

The browser owns

  • the single choice among radios with one name
  • the arrow keys, wrapping and skipping disabled radios, and the single tab stop
  • the group name from <legend> and each segment’s name from its label
  • the form value and the change event

Svelte Lean owns

  • segmented.css: equal-width segments in one bar, the raised checked segment, the focus ring on the segment, disabled segments
  • the contract
  • documentation

Usage

The markup needs no package. Install @svelte-lean/styles for the stylesheet; @svelte-lean/primitives adds the typed contract and nothing at runtime.

npm install @svelte-lean/styles
stylesheets
import '@svelte-lean/styles/segmented.css';

Put data-slean="segmented" on a <fieldset> with a <legend>, and one <label> per choice holding a radio and its text. Give every radio the same name and check one.

Bind the value with bind:group, read it from FormData, or listen to the native change event.

Use it for two to five short choices that take effect at once or are submitted with a form. For panels of content use Tabs; for buttons that are pressed independently use Toggle group.

Anatomy

PartElementdata-slean-partRequiredNotes
root<fieldset data-slean="segmented">–yesNamed by its <legend>, or by aria-label when the bar has no visible label.
segment<label><input type="radio" name="…" value="…"> Text</label>–yesTwo to five, one checked. The radio covers the label, transparent and focusable.

Runtime profile

Tier 0: the segmented control has no behavior module, the Vite plugin maps segmented to no module, and the native-only fixture proves the path ships no Svelte Lean JavaScript.

Accessibility contract

  • The fieldset is a group named by its legend, or by aria-label when the bar has no visible label; each radio is named by its label.
  • The group is one tab stop: the checked radio. The arrow keys check the next or previous radio, wrap, and skip disabled ones.
  • The focus ring is drawn on the segment of the focused radio, only for keyboard focus (:focus-visible).
  • An icon-only segment keeps a text name: aria-label on its input, the icon aria-hidden.
  • Forced colors: the checked segment takes the Highlight system color and every segment an outline.

Keyboard

KeyWhenResult
Tabon the pageEnters the group at the checked radio; the next Tab leaves it (native)
ArrowRight/ArrowDownfocus in the groupChecks the next segment, wrapping (native; mirrored under RTL in Chrome)
ArrowLeft/ArrowUpfocus in the groupChecks the previous segment, wrapping (native)
Spacefocus on an unchecked radioChecks it (native)

Platform features

FeatureBaselineOutside the target
Radios, <fieldset>, <legend>Widely availableNot applicable
:has()Baseline 2023 (Chrome 105, Safari 15.4, Firefox 121)The checked segment is not drawn raised; the radios keep working

Without JavaScript

Fully functional.

Server rendering

The checked attribute renders the initial choice; the raised segment is drawn by CSS from it, before any script runs.

Before hydration

Nothing is attached; the radios work before and after hydration alike.

Styling

segmented.css lays the labels out as equal-width grid columns on a muted bar, raises the checked segment with a surface, a hairline and a small shadow (a ::before inset behind the text), draws the focus ring on that surface, and dims disabled segments. The tokens it reads:

TokenDefault (light)Applies to
--slean-space-20.5remmargin-block-end, gap
--slean-fgvar(--slean-neutral-12)color
--slean-text-sm0.875remfont-size
--slean-font-weight-medium500font-weight
--slean-control-height-md2.25remmin-block-size
--slean-space-41rempadding-inline
--slean-mutedvar(--slean-neutral-3)background
--slean-fg-mutedvar(--slean-neutral-11)color
--slean-duration-fast100mstransition
--slean-easecubic-bezier(0.2, 0, 0, 1)transition
--slean-radius-md0.625remborder-start-start-radius, border-end-start-radius, border-start-end-radius, border-end-end-radius
--slean-radius-sm0.375remborder-radius
--slean-surfaceoklch(100% 0 0)background
--slean-bordervar(--slean-neutral-6)box-shadow
--slean-shadow-sm0 1px 2px oklch(0% 0 0 / 0.08)box-shadow
--slean-focus-ringvar(--slean-focus-ring-width) solid var(--slean-focus-ring-color)outline

State selectors the stylesheet targets, all from the platform or ARIA: :checked, :disabled, :first-of-type, :focus-visible, :hover, :last-of-type, [type="radio"].

Compatibility notes

Radios and fieldsets are widely available. The checked, focused and disabled segments are selected with :has(), Baseline 2023; where it is missing the radios work and the segments do not show the choice.

Examples

Disabled segments and a hidden label

disabled on a radio dims its segment and the arrow keys skip it; disabled on the fieldset disables the bar. Without a visible legend, the fieldset is named by aria-label.

Density
states
<fieldset data-slean="segmented" aria-label="Alignment">
	<label><input type="radio" name="segmented-align" value="start" checked /> Start</label>
	<label><input type="radio" name="segmented-align" value="center" /> Center</label>
	<label><input type="radio" name="segmented-align" value="end" disabled /> End</label>
</fieldset>

<fieldset data-slean="segmented" disabled>
	<legend>Density</legend>
	<label><input type="radio" name="segmented-density" value="compact" /> Compact</label>
	<label><input type="radio" name="segmented-density" value="comfortable" checked />
		Comfortable</label>
</fieldset>

Testing

Source