Segmented controlDesign system
ComponentProduct

Segmented control

Single-choice option group with radio semantics and an animated selection indicator.

npx context-ds add segmented-control
In the product

Usage

Use it for

Ranges on charts, small choices inside forms. segmented (a gray track) where the unpicked options must still read as choices.

Dependencies: use-tab-glide

Example

examples/segmented-control.tsx
"use client";import { useState } from "react";import { SegmentedControl } from "@/components/ui/segmented-control";export function Product() {  const [range, setRange] = useState<"24h" | "7d" | "30d">("7d");  const [view, setView] = useState<"requests" | "credits">("requests");  return (    <div className="flex flex-col items-start gap-4">      <SegmentedControl        label="Range"        value={range}        onValueChange={setRange}        options={[          { value: "24h", label: "24h" },          { value: "7d", label: "7 days" },          { value: "30d", label: "30 days" },        ]}      />      <SegmentedControl        label="Chart"        variant="segmented"        value={view}        onValueChange={setView}        options={[          { value: "requests", label: "Requests" },          { value: "credits", label: "Credits" },        ]}      />    </div>  );}

Source

components/ui/segmented-control.tsx
"use client";import * as React from "react";import { useTabGlide } from "@/hooks/use-tab-glide";import { cn } from "@/lib/utils";// The dashboard's tabs used as a value picker inside a form or a chart (form.tsx SegmentedControl): the same gliding// soft gray pill, with radio semantics. `segmented` sits the options on a gray track with a white pill.type Option<T extends string> = { value: T; label: React.ReactNode; count?: number };const INNER_RADIUS = "rounded-[calc(var(--ds-radius-control)-0.125rem)]";const PILL_SURFACE = { plain: "bg-wash-pill", segmented: "bg-surface shadow-[0_1px_3px_rgb(0_0_0/0.08),0_0_0_1px_rgb(0_0_0/0.03)]" };function SegmentedControl<T extends string>({  value,  onValueChange,  options,  label,  size = "md",  variant = "plain",  className,}: {  value: T;  onValueChange: (value: T) => void;  options: Option<T>[];  label: string;  size?: "sm" | "md";  variant?: "plain" | "segmented";  className?: string;}) {  const pillRef = React.useRef<HTMLSpanElement>(null);  const isPlaced = useTabGlide(pillRef, value);  const segmented = variant === "segmented";  const radius = size === "sm" || segmented ? INNER_RADIUS : "rounded-control";  return (    <div      role="radiogroup"      aria-label={label}      data-slot="segmented-control"      className={cn("relative isolate inline-flex w-fit items-center", segmented ? "gap-0.5 rounded-control bg-neutral-5 p-0.5" : size === "sm" ? "gap-0.5" : "gap-1", className)}    >      <span ref={pillRef} aria-hidden className={cn("pointer-events-none absolute top-0 left-0 -z-10", radius, PILL_SURFACE[variant], !isPlaced && "invisible")} />      {options.map((o, index) => {        const selected = o.value === value;        return (          <button            key={o.value}            type="button"            role="radio"            aria-checked={selected}            tabIndex={selected || (!options.some((option) => option.value === value) && index === 0) ? 0 : -1}            data-tab-id={o.value}            onClick={() => onValueChange(o.value)}            onKeyDown={(event) => {              const delta = event.key === "ArrowRight" || event.key === "ArrowDown" ? 1 : event.key === "ArrowLeft" || event.key === "ArrowUp" ? -1 : 0;              if (!delta && event.key !== "Home" && event.key !== "End") return;              event.preventDefault();              const next = event.key === "Home" ? 0 : event.key === "End" ? options.length - 1 : (index + delta + options.length) % options.length;              onValueChange(options[next].value);              event.currentTarget.parentElement?.querySelectorAll<HTMLButtonElement>('[role="radio"]')[next]?.focus();            }}            className={cn(              "flex items-center gap-1.5 whitespace-nowrap outline-none transition-[color] duration-200 ease-out focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-solid focus-visible:outline-focus-ring motion-reduce:transition-none",              radius,              size === "sm" ? "h-7 px-3 text-caption" : segmented ? "h-8 px-3.5 text-body-sm" : "h-9 px-4 text-body-sm",              // Only the text answers a hover, so nothing fades over the pill gliding in beneath.              selected ? "text-fg" : cn("hover:text-fg", segmented ? "text-fg-muted" : "text-fg-subtle"),              // Until the pill lands, the selected option draws the surface itself so the first paint matches.              selected && !isPlaced && PILL_SURFACE[variant],            )}          >            {o.label}            {o.count !== undefined && <span className={cn("tabular-nums transition-[color] duration-200 ease-out", selected ? "text-fg-muted" : "text-fg-subtle")}>{o.count}</span>}          </button>        );      })}    </div>  );}export { SegmentedControl };

Reference: context-dashboard c0ddf7a src/components/ui/tabs.tsx; src/components/ui/form.tsx.