Controlsradix-ui
Segmented control
The iOS segmented control: one raised thumb slides to the chosen segment; always has a selection.
Installation
pnpm dlx shadcn@latest add https://glasscn.app/r/segmented-control.jsonOr with the namespace set up: shadcn add @glasscn/segmented-control
Props
| Prop | Type | Default |
|---|---|---|
| value / defaultValue | stringThe chosen segment. Controlled or not. | — |
| onValueChange | (value: string) => voidFires with the new segment. Never fires with an empty value — a segmented control always has a selection. | — |
| size | "sm" | "default" | "lg" | "default" |
Source
components/glass/segmented-control.tsx
"use client"
import * as React from "react"
import { cn } from "cn"
import { ToggleGroup as ToggleGroupPrimitive } from "radix-ui"
import { slidingIndicatorClass, useSlidingIndicator } from "@/hooks/use-sliding-indicator"
type SegmentedControlProps = Omit<
React.ComponentProps<typeof ToggleGroupPrimitive.Root>,
"type" | "value" | "defaultValue" | "onValueChange"
> & {
value?: string
defaultValue?: string
onValueChange?: (value: string) => void
size?: "sm" | "default" | "lg"
}
/**
* The iOS segmented control: a soft track with one raised thumb that slides
* to the chosen segment. Always has a selection — tapping the chosen segment
* again keeps it (unlike a toggle group).
*/
function SegmentedControl({
className,
value,
defaultValue,
onValueChange,
size = "default",
children,
...props
}: SegmentedControlProps) {
const [inner, setInner] = React.useState(defaultValue ?? "")
const current = value ?? inner
const ref = useSlidingIndicator<HTMLDivElement>('[data-slot="segmented-control-item"][data-state="on"]')
return (
<ToggleGroupPrimitive.Root
ref={ref}
type="single"
data-slot="segmented-control"
data-size={size}
value={current}
onValueChange={(v: string) => {
if (!v) return
setInner(v)
onValueChange?.(v)
}}
className={cn(
"group/segmented relative inline-flex w-fit items-stretch rounded-control bg-fill p-0.5 data-[size=default]:h-[calc(2.25rem*var(--glass-density))] data-[size=lg]:h-[calc(2.75rem*var(--glass-density))] data-[size=sm]:h-[calc(1.75rem*var(--glass-density))]",
className
)}
{...props}
>
<span
data-slot="segmented-control-thumb"
aria-hidden
className={cn(
slidingIndicatorClass,
"rounded-control-sm bg-glass-thumb shadow-[0_3px_8px_rgb(0_0_0/0.12),0_0_1px_rgb(0_0_0/0.1)] dark:shadow-none"
)}
/>
{children}
</ToggleGroupPrimitive.Root>
)
}
function SegmentedControlItem({ className, ...props }: React.ComponentProps<typeof ToggleGroupPrimitive.Item>) {
return (
<ToggleGroupPrimitive.Item
data-slot="segmented-control-item"
className={cn(
"relative z-10 inline-flex min-w-0 flex-1 items-center justify-center gap-1.5 rounded-control-sm px-3 text-sm font-medium whitespace-nowrap text-foreground/70 transition-colors outline-none hover:text-foreground focus-visible:ring-(length:--glass-ring-width) focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-40 data-[state=on]:font-semibold data-[state=on]:text-foreground group-data-[size=lg]/segmented:text-base group-data-[size=sm]/segmented:px-2 group-data-[size=sm]/segmented:text-xs [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
/>
)
}
export { SegmentedControl, SegmentedControlItem }