Segmented Control
A bank of positions milled into one housing — a debossed carriage glides between segments with a sprung settle.
@emboss/segmented-control
MonoStereoWide
Installation
pnpm dlx shadcn@latest add @emboss/segmented-controlUsage
import {
SegmentedControl,
SegmentedControlItem,
} from "@/components/ui/segmented-control";
export function Example() {
return (
<SegmentedControl defaultValue="m" aria-label="Size">
<SegmentedControlItem value="s">S</SegmentedControlItem>
<SegmentedControlItem value="m">M</SegmentedControlItem>
<SegmentedControlItem value="l">L</SegmentedControlItem>
</SegmentedControl>
);
}API reference
SegmentedControl
Radio-group semantics: exactly one segment is always engaged. Label the group with aria-label.
| Prop | Type | Default | Description |
|---|---|---|---|
| value / defaultValue / onValueChange | string / string / (value) => void | — | Controlled and uncontrolled selection. |
SegmentedControlItem
| Prop | Type | Default | Description |
|---|---|---|---|
| value | string | — | The position this segment represents. |
| disabled | boolean | false | Removes the segment from selection. |
| Data attribute | Description |
|---|---|
| data-checked | Present on the engaged segment. |
Keyboard
| Keys | Action |
|---|---|
| ArrowLeftArrowRight | Moves the carriage to the previous or next segment. |
| Space | Engages the focused segment. |
Source
View source — segmented-control.tsxHide source — segmented-control.tsx
"use client";
import { RadioGroup as BaseRadioGroup } from "@base-ui/react/radio-group";
import { Radio as BaseRadio } from "@base-ui/react/radio";
import { motion, useReducedMotion } from "motion/react";
import * as React from "react";
import { springs } from "@/lib/springs";
import { cn } from "@/lib/utils";
const SegmentedContext = React.createContext<string | null>(null);
export type SegmentedControlProps = Omit<
React.ComponentProps<typeof BaseRadioGroup>,
"value" | "defaultValue" | "onValueChange"
> & {
value?: string;
defaultValue?: string;
onValueChange?: (value: string) => void;
};
/**
* A bank of positions milled into one housing. Exactly one segment is
* always engaged: a debossed carriage sits latched under it and glides to
* the next position with a sprung settle.
*
* @example
* <SegmentedControl defaultValue="m" aria-label="Channel width">
* <SegmentedControlItem value="s">Mono</SegmentedControlItem>
* <SegmentedControlItem value="m">Stereo</SegmentedControlItem>
* <SegmentedControlItem value="w">Wide</SegmentedControlItem>
* </SegmentedControl>
*/
function SegmentedControl({
className,
value,
defaultValue,
onValueChange,
...props
}: SegmentedControlProps) {
const carriageId = React.useId();
return (
<SegmentedContext.Provider value={carriageId}>
<BaseRadioGroup
data-slot="segmented-control"
value={value}
defaultValue={defaultValue}
onValueChange={(next) => {
if (typeof next === "string") onValueChange?.(next);
}}
className={cn(
"inline-flex w-fit items-center gap-1 rounded-md bg-surface-2 p-1 shadow-emboss-1",
className,
)}
{...props}
/>
</SegmentedContext.Provider>
);
}
function SegmentedControlItem({
className,
children,
...props
}: React.ComponentProps<typeof BaseRadio.Root>) {
const carriageId = React.useContext(SegmentedContext);
const reduceMotion = useReducedMotion() ?? false;
return (
<BaseRadio.Root
data-slot="segmented-control-item"
className={cn(
"relative inline-flex h-7 cursor-pointer items-center justify-center rounded-sm px-3 text-sm font-medium whitespace-nowrap text-ink-muted focus-ring transition-colors duration-(--duration-settle) hover:text-ink data-checked:text-ink data-disabled:pointer-events-none data-disabled:opacity-50",
className,
)}
{...props}
>
<BaseRadio.Indicator
keepMounted={false}
data-slot="segmented-control-indicator"
className="absolute inset-0"
>
<motion.span
aria-hidden
layoutId={carriageId ?? undefined}
transition={reduceMotion ? { duration: 0 } : springs.settle}
className="absolute inset-0 rounded-sm bg-well shadow-deboss-1"
/>
</BaseRadio.Indicator>
<span className="relative z-10">{children}</span>
</BaseRadio.Root>
);
}
export { SegmentedControl, SegmentedControlItem };