- Accordion
- Alert
- Alert Dialog
- Autocomplete
- Avatar
- Badge
- Breadcrumb
- Button
- Calendar
- Card
- Checkbox
- Checkbox Group
- Collapsible
- Combobox
- Command
- Context Menu
- Date Picker
- Dialog
- Drawer
- Empty
- Field
- Fieldset
- Form
- Frame
- Group
- Input
- Input Group
- Kbd
- Label
- Menu
- Meter
- Number Field
- OTP Field
- Pagination
- Popover
- Preview Card
- Progress
- Radio Group
- Scroll Area
- Select
- Segmented ControlNew
- Separator
- Sheet
- Skeleton
- Slider
- Spinner
- Switch
- Table
- Tabs
- Textarea
- Toast
- Toggle
- Toggle Group
- Toolbar
- Tooltip
Segmented Control
A visual pattern for presenting related choices, navigation destinations, filters, or content views.
"use client";
import {
segmentedControlItemVariants,
segmentedControlRootClassName,
} from "@/lib/segmented-control";
import {
RadioGroupPrimitive,
RadioPrimitive,
} from "@/components/ui/radio-group";
const itemClassName = segmentedControlItemVariants({
className: "grow",
state: "checked",
});
export default function Particle() {
return (
<RadioGroupPrimitive
aria-label="Billing period"
className={segmentedControlRootClassName}
defaultValue="monthly"
>
<RadioPrimitive.Root className={itemClassName} value="monthly">
Monthly
</RadioPrimitive.Root>
<RadioPrimitive.Root className={itemClassName} value="yearly">
Yearly
</RadioPrimitive.Root>
</RadioGroupPrimitive>
);
}
About
A segmented control is a visual pattern, not a standalone behavior. COSS uses the same presentation across several components while preserving the semantics, keyboard interactions, and state model of each underlying primitive.
Choose the right primitive
| Intent | Use | Why |
|---|---|---|
| Choose one value in a form | Radio Group | Represents a mutually exclusive value and participates in form state. |
| Navigate to another URL or route | Navigation links | Preserves link behavior, browser history, and aria-current. |
| Apply an exclusive filter or mode | Toggle Group | Represents the pressed state of an action that may be cleared. |
| Switch between related panels | Tabs | Connects each tab to an associated content panel. |
Choose the primitive from the interaction first, then apply the segmented-control styling. Visual similarity alone is not a reason to use Tabs or Toggle Group.
Installation
Segmented controls are provided as particles. Install the implementation and size that match your interaction:
| Implementation | Small | Default | Large |
|---|---|---|---|
| Radio Group | @coss/p-radio-group-7 | @coss/p-radio-group-8 | @coss/p-radio-group-9 |
| Navigation | @coss/p-navigation-2 | @coss/p-navigation-1 | @coss/p-navigation-3 |
For example, install the default Radio Group version with:
pnpm dlx shadcn@latest add @coss/p-radio-group-8
The CLI installs the shared segmented-control styling library and the required primitive automatically.
Shared styling
For a custom composition, install the styling library directly:
pnpm dlx shadcn@latest add @coss/segmented-control
The library exports a root class and an item recipe:
import {
segmentedControlItemVariants,
segmentedControlRootClassName,
} from "@/lib/segmented-control"const itemClassName = segmentedControlItemVariants({
size: "default",
state: "checked",
})| Option | Values | Description |
|---|---|---|
size | "sm" | "default" | "lg" | Controls item height and horizontal padding. |
state | "checked" | "current" | "pressed" | Selects the state attribute used by the underlying element. |
Use checked with Radio Group, current with navigation links, and pressed with Toggle Group. Tabs retain their own animated indicator and do not use the shared state recipe.
At the outside edges, the item padding and the surface's p-0.5 inset combine to match the horizontal padding of the corresponding Button size. The outer segmented surface is slightly taller than that Button to optically balance its inset selected item when the controls appear next to each other.
import { cva } from "class-variance-authority";
export type SegmentedControlSize = "default" | "lg" | "sm";
export const segmentedControlItemSizeClassNames: Record<
SegmentedControlSize,
string
> = {
default: "h-8.5 px-[calc(--spacing(2.5)-1px)] sm:h-7.5",
lg: "h-9.5 px-[calc(--spacing(3)-1px)] sm:h-8.5",
sm: "h-7.5 px-[calc(--spacing(2)-1px)] sm:h-6.5",
};
export const segmentedControlRootClassName =
"relative z-0 flex w-fit items-center justify-center gap-0.5 rounded-lg bg-muted p-0.5";
export const segmentedControlItemVariants = cva(
"relative inline-flex shrink-0 cursor-pointer select-none items-center justify-center whitespace-nowrap rounded-md border border-transparent font-medium text-base text-muted-foreground/72 outline-2 outline-transparent transition-[outline-color] hover:bg-transparent hover:text-muted-foreground focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-64 data-disabled:pointer-events-none data-disabled:opacity-64 sm:text-sm",
{
defaultVariants: {
size: "default",
},
variants: {
size: segmentedControlItemSizeClassNames,
state: {
checked:
"data-checked:bg-background data-checked:text-foreground data-checked:shadow-sm/5 dark:data-checked:bg-input",
current:
"aria-[current=page]:bg-background aria-[current=page]:text-foreground aria-[current=page]:shadow-sm/5 dark:aria-[current=page]:bg-input",
pressed:
"data-pressed:bg-background data-pressed:text-foreground data-pressed:shadow-sm/5 dark:data-pressed:bg-input",
},
},
},
);
Radio options
Use Radio Group when the selected segment represents a mutually exclusive value, especially in forms.
Small Radio Group
"use client";
import {
segmentedControlItemVariants,
segmentedControlRootClassName,
} from "@/lib/segmented-control";
import {
RadioGroupPrimitive,
RadioPrimitive,
} from "@/components/ui/radio-group";
const itemClassName = segmentedControlItemVariants({
className: "grow",
size: "sm",
state: "checked",
});
export default function Particle() {
return (
<RadioGroupPrimitive
aria-label="Billing period"
className={segmentedControlRootClassName}
defaultValue="monthly"
>
<RadioPrimitive.Root className={itemClassName} value="monthly">
Monthly
</RadioPrimitive.Root>
<RadioPrimitive.Root className={itemClassName} value="yearly">
Yearly
</RadioPrimitive.Root>
</RadioGroupPrimitive>
);
}
Default Radio Group
"use client";
import {
segmentedControlItemVariants,
segmentedControlRootClassName,
} from "@/lib/segmented-control";
import {
RadioGroupPrimitive,
RadioPrimitive,
} from "@/components/ui/radio-group";
const itemClassName = segmentedControlItemVariants({
className: "grow",
state: "checked",
});
export default function Particle() {
return (
<RadioGroupPrimitive
aria-label="Billing period"
className={segmentedControlRootClassName}
defaultValue="monthly"
>
<RadioPrimitive.Root className={itemClassName} value="monthly">
Monthly
</RadioPrimitive.Root>
<RadioPrimitive.Root className={itemClassName} value="yearly">
Yearly
</RadioPrimitive.Root>
</RadioGroupPrimitive>
);
}
Large Radio Group
"use client";
import {
segmentedControlItemVariants,
segmentedControlRootClassName,
} from "@/lib/segmented-control";
import {
RadioGroupPrimitive,
RadioPrimitive,
} from "@/components/ui/radio-group";
const itemClassName = segmentedControlItemVariants({
className: "grow",
size: "lg",
state: "checked",
});
export default function Particle() {
return (
<RadioGroupPrimitive
aria-label="Billing period"
className={segmentedControlRootClassName}
defaultValue="monthly"
>
<RadioPrimitive.Root className={itemClassName} value="monthly">
Monthly
</RadioPrimitive.Root>
<RadioPrimitive.Root className={itemClassName} value="yearly">
Yearly
</RadioPrimitive.Root>
</RadioGroupPrimitive>
);
}
Navigation
Use links when each segment points to a different destination. Apply aria-current="page" to the active link.
Small Navigation
import {
segmentedControlItemVariants,
segmentedControlRootClassName,
} from "@/lib/segmented-control";
const itemClassName = segmentedControlItemVariants({
size: "sm",
state: "current",
});
export default function Particle() {
return (
<nav aria-label="Project sections">
<div className={segmentedControlRootClassName}>
<a aria-current="page" className={itemClassName} href="#overview">
Overview
</a>
<a className={itemClassName} href="#activity">
Activity
</a>
<a className={itemClassName} href="#settings">
Settings
</a>
</div>
</nav>
);
}
Default Navigation
import {
segmentedControlItemVariants,
segmentedControlRootClassName,
} from "@/lib/segmented-control";
const itemClassName = segmentedControlItemVariants({ state: "current" });
export default function Particle() {
return (
<nav aria-label="Project sections">
<div className={segmentedControlRootClassName}>
<a aria-current="page" className={itemClassName} href="#overview">
Overview
</a>
<a className={itemClassName} href="#activity">
Activity
</a>
<a className={itemClassName} href="#settings">
Settings
</a>
</div>
</nav>
);
}
Large Navigation
import {
segmentedControlItemVariants,
segmentedControlRootClassName,
} from "@/lib/segmented-control";
const itemClassName = segmentedControlItemVariants({
size: "lg",
state: "current",
});
export default function Particle() {
return (
<nav aria-label="Project sections">
<div className={segmentedControlRootClassName}>
<a aria-current="page" className={itemClassName} href="#overview">
Overview
</a>
<a className={itemClassName} href="#activity">
Activity
</a>
<a className={itemClassName} href="#settings">
Settings
</a>
</div>
</nav>
);
}
Related content
Use Tabs when each segment controls an associated content panel. Tabs share the visual language of segmented controls but keep their animated indicator, orientation support, and panel semantics.
Tab 1 content
import { Tabs, TabsList, TabsPanel, TabsTab } from "@/components/ui/tabs";
export default function Particle() {
return (
<Tabs defaultValue="tab-1">
<TabsList>
<TabsTab value="tab-1">Tab 1</TabsTab>
<TabsTab value="tab-2">Tab 2</TabsTab>
<TabsTab value="tab-3">Tab 3</TabsTab>
</TabsList>
<TabsPanel value="tab-1">
<p className="p-4 text-center text-muted-foreground text-xs">
Tab 1 content
</p>
</TabsPanel>
<TabsPanel value="tab-2">
<p className="p-4 text-center text-muted-foreground text-xs">
Tab 2 content
</p>
</TabsPanel>
<TabsPanel value="tab-3">
<p className="p-4 text-center text-muted-foreground text-xs">
Tab 3 content
</p>
</TabsPanel>
</Tabs>
);
}