Skip to content
Liquidify
HomeDocsComponentsExamplesPlayground
⌘K
(opens in new tab)
Liquidify

Accessible React controls with a CSS and SVG material engine.

Product

ComponentsExamplesPlaygroundPackages

Learn

DocumentationGetting startedConceptsAI resources

Project

CommunityShowcaseVersionsGitHub

Independent open-source software. Not affiliated with or endorsed by Apple Inc.

LicenseBrand

Component catalog

Browse the public controls by interaction purpose. Every result links to generated API facts, accessibility behavior, source, and exhaustive stories.

SegmentedControl

A horizontal track of 2–N mutually-exclusive segments where exactly one is selected — the canonical glass segmented picker.

import { SegmentedControl } from "@liquidify/react/segmented-control"

Overview

Basic Segmented Control

A focused starting point showing Segmented Control with its essential configuration.

API

PropTypeDefaultDescription
children
required
ReactNode—The segments — 2..N {@link SegmentedControl.Item} children, rendered equal-width (children role: `items`). Only `SegmentedControl.Item` children are read; any non-`Item` node (a `Fragment`, a wrapper, stray text) is a **dev-mode `console.error`** and is filtered in production. Each `Item`'s `value` is the collection identity (resolved through `shared/collection`) — duplicate `value`s are a dev-mode error (first wins in production); duplicate visible **labels** are always supported.
classNamestring | undefined—Optional additional CSS class merged into the glass surface class. The supported per-instance customisation seam: override any `--lq-*` custom property from this class's CSS — the token cascade the engine reads (ADR-0004). No raw inline style is involved, so this stays ADR-0003 clean.
defaultValuestring | undefinedthe first child's `value`Uncontrolled seed for the internal selection (read once at mount; ignored once {@link SegmentedControlProps.value} is supplied). A seed that matches no `.Item` resolves to no selection, like a missing controlled `value`.
disabledboolean | undefined—When `true`, the whole control is dimmed, removed from the tab order (every segment `tabIndex={-1}`), and all interaction is suppressed. Since the root is a `<div role="radiogroup">` (no native `disabled`), this sets `aria-disabled` plus a `data-disabled` styling hook.
distribution"fill" | "fit" | undefinedfillWidth distribution — the `NSSegmentedControl.Distribution` analogue. `"fill"` (the default) makes the control **fill its container** (`display: grid`); `"fit"` makes it **hug its content** (`display: inline-grid`). Either way the segments stay equal-width; only container-fill vs content-hug differs. Resolved via the `data-distribution` attribute selector in `styles.css`.
labelPropsLabelProps | undefined—Per-instance typography override for the segment labels, forwarded to the internal {@link Text} (ADR-0028). Token-backed axes only; the HIG default is `variant="body"` with `color="inherit"`, so each label adopts the segment's own selected/unselected colour (system-gray-1 unselected, primary selected). Applies only to string/number segment labels — an `<Icon>` segment passes through unwrapped.
onChange((next: string) => void) | undefined—Fires with the next selected item's `value` whenever the selection moves — pointer tap, drag-release, or a keyboard arrow / Home / End. Runs through the shared {@link useControllableState} triad, so it fires **at most once per accepted change** in both controlled and uncontrolled modes: a no-op re-selection, a disabled interaction, and a parent-driven controlled `value` change all emit nothing.
refRef<HTMLDivElement> | undefined—Forwarded ref to the rendered element — the component's native root (or the merged child element on a component that opts into {@link SlottableProps}). Generic over `E` (charter `docs/api-conventions.md` §4 rule 1, default `HTMLElement`) so a component with a polymorphic root (Link's navigation / action mode union, Text's `as`) can instantiate the concrete element per variant instead of leaving every consumer with a widened `Ref<HTMLElement>`. A component that does not narrow simply inherits the `HTMLElement` default — source-compatible with every pre-existing non-generic `extends ControlProps`.
sizeSegmentedControlSize | undefinedregularSize of the control — the full canonical {@link SegmentedControlSize} scale (`mini · small · regular · large · xlarge`). SegmentedControl is the exception that ships all five sizes (owner change). Resolved to geometry via the `data-size` attribute selector in `styles.css`; across the five sizes only the box scales — the label font-size stays constant.
valuestring | undefined—Controlled selected value. Passing this switches SegmentedControl to controlled mode: the rendered selection always reflects this value and the consumer owns it via {@link SegmentedControlProps.onChange}. A `value` that matches no `.Item` resolves to **no selection** (no segment `aria-checked`, the pill hidden at index 0, roving `tabIndex` on the first enabled segment) — it never silently selects index 0. Switching between controlled and uncontrolled after mount is a dev-mode `console.error`.

Guidelines

Do
  • Use Segmented Control when its semantics match the content or action.
  • Keep labels concise, specific, and consistent with the surrounding interface.
  • Test the component with keyboard navigation, zoom, and both color schemes.
Don't
  • Do not use Segmented Control as a visual substitute for a different semantic element.
  • Do not rely on color or an icon alone to communicate essential meaning.
  • Do not override the component tokens with fixed values unless the product theme requires it.

Accessibility

SegmentedControl supports the documented keyboard interactions below.

Keyboard
  • Press Tab to move focus to the control.
  • Use the control's documented activation keys without trapping focus.
  • Provide an accessible name whenever the visible label is absent.

Tokens

NameValue
--lq-color-accent#007aff
--lq-color-bg-primary#ffffff
--lq-color-label-primaryComponent-defined value
--lq-color-label-secondaryComponent-defined value
--lq-glass-440.71.15var(--lq-segmented-control-indicator-blur)calc(var(--lq-glass-material-blur) * 8)var(--lq-shadow-presentational)
--lq-glass-fill-0Resolved by the active Liquidify theme
--lq-hit-target-touch2.75rem
--lq-lens-progressResolved by the active Liquidify theme
--lq-lens-stretchResolved by the active Liquidify theme
--lq-lens-xResolved by the active Liquidify theme
--lq-radius-0.25rem0.375rem0.5rem0.625rem0.875rem1.125rem1.375rem9999rem
--lq-radius-pill9999rem
--lq-segmented-control-var(--lq-space-40)var(--lq-hit-target-min)var(--lq-space-32)var(--lq-space-48)var(--lq-space-56)var(--lq-space-4)var(--lq-font-size-body)calc(var(--lq-glass-material-blur) * 8)calc(var(--lq-space-24) - var(--lq-space-2))var(--lq-radius-pill)var(--lq-shadow-glass-edge), var(--lq-shadow-interactive-active)0.4
--lq-segmented-control-bloom-wcalc( (var(--lq-segmented-control-lens-w, var(--lq-space-48)) - var(--lq-segmented-control-rest-w, var(--lq-space-32))) * var(--lq-lens-progress, 0) )
--lq-segmented-control-col-wcalc(100% / 2 - 2 * var(--lq-segmented-control-pad, var(--lq-space-2)))calc(100% / 3 - 2 * var(--lq-segmented-control-pad, var(--lq-space-2)))calc(100% / 4 - 2 * var(--lq-segmented-control-pad, var(--lq-space-2)))calc(100% / 5 - 2 * var(--lq-segmented-control-pad, var(--lq-space-2)))calc(100% / 6 - 2 * var(--lq-segmented-control-pad, var(--lq-space-2)))calc(100% / 7 - 2 * var(--lq-segmented-control-pad, var(--lq-space-2)))calc(100% / 8 - 2 * var(--lq-segmented-control-pad, var(--lq-space-2)))
--lq-segmented-control-indicator-alphaComponent-defined value
--lq-segmented-control-insetComponent-defined value
--lq-segmented-control-label-sizevar(--lq-font-size-body)
--lq-segmented-control-lens-calc(var(--lq-segmented-control-rest-h, var(--lq-space-24)) * 1.5)calc( var(--lq-segmented-control-rest-w, var(--lq-space-32)) + (var(--lq-segmented-control-lens-h, var(--lq-space-40)) - var(--lq-segmented-control-rest-h, var(--lq-space-24))) )
--lq-segmented-control-padvar(--lq-space-4)
--lq-segmented-control-rest-calc( var(--lq-segmented-control-track-h, var(--lq-space-32)) - 2 * var(--lq-segmented-control-pad, var(--lq-space-2)) )calc(var(--lq-segmented-control-rest-h, var(--lq-space-24)) * 1.4)
--lq-segmented-control-track-hvar(--lq-space-40)var(--lq-hit-target-min)var(--lq-space-32)var(--lq-space-48)var(--lq-space-56)
--lq-shadow-ambient-10 0.125rem 0.375rem rgba(0, 0, 0, 0.10)
--lq-space-0.0625rem0.125rem0.25rem0.5rem0.75rem1rem1.25rem1.5rem2rem2.5rem3rem3.5rem
--lq-toggle-var(--lq-space-2)var(--lq-toggle-pad, var(--lq-space-2))0.55calc(var(--lq-space-64) + var(--lq-space-8))calc(var(--lq-space-40) + var(--lq-space-4))var(--lq-space-32)calc(var(--lq-space-16) + var(--lq-space-4))calc(var(--lq-toggle-track-h, var(--lq-space-32)) - 2 * var(--lq-toggle-pad, var(--lq-space-2)))var(--lq-space-24)var(--lq-thumb-lens-h)var(--lq-thumb-lens-w)calc( var(--lq-toggle-track-w, var(--lq-space-56)) - 2 * var(--lq-toggle-hpad, var(--lq-space-2)) - var(--lq-toggle-thumb-w, var(--lq-space-32)) )

Key features

  • Liquid material

    Uses Liquidify surfaces and theme-aware design tokens.

  • Typed API

    Provides a typed React API with generated prop documentation.

  • Accessible by default

    Preserves semantic structure, focus visibility, and assistive-technology state.

Resources

  • Source
  • Markdown
  • JSON
  • Storybook
Components