SegmentedControl
Pick one of a few options — a sliding indicator, icons and the same sizes as Button.
'use client';
import { SegmentedControl } from '@akhatmirzo/ui';
export default function SegmentedControlBasic() {
return (
<SegmentedControl defaultValue="month" aria-label="Davr">
<SegmentedControl.Item value="day">Kun</SegmentedControl.Item>
<SegmentedControl.Item value="week">Hafta</SegmentedControl.Item>
<SegmentedControl.Item value="month">Oy</SegmentedControl.Item>
<SegmentedControl.Item value="year">Yil</SegmentedControl.Item>
</SegmentedControl>
);
}
Import
import { SegmentedControl } from '@akhatmirzo/ui';When to use
- View modes — "Grid / List / Map". The choice applies immediately.
- Filters or periods — "Day / Week / Month", "All / Paid / Unpaid".
- Keep it to 2–5 short options. For more or longer ones use a
Selector aRadioGroup. - Not for on/off — a two-state setting is a
Switch.
How it differs from Tabs
They can look the same, but they do different jobs:
Tabs | SegmentedControl | |
|---|---|---|
| What it does | Switches content panels | Picks a value (filter, mode, period) |
| Semantics | tablist / tab / tabpanel | radiogroup / radio |
| Result | A different section of the page shows | The same content is refiltered or redrawn |
| Forms | No | Submits a value with name |
Rule of thumb: if the choice reveals different content, use Tabs; if the same
content changes how it looks or what it shows, use SegmentedControl.
Sizes
size — sm (32px), md (40px, default), lg (48px). The heights match Button
and Input, so they line up in one row.
'use client';
import { Button, SegmentedControl } from '@akhatmirzo/ui';
const sizes = ['sm', 'md', 'lg'] as const;
export default function SegmentedControlSizes() {
return (
<div className="flex flex-col items-start gap-4">
{sizes.map((size) => (
<div key={size} className="flex items-center gap-3">
<SegmentedControl size={size} defaultValue="all" aria-label={`Holat, ${size}`}>
<SegmentedControl.Item value="all">Hammasi</SegmentedControl.Item>
<SegmentedControl.Item value="paid">To'langan</SegmentedControl.Item>
<SegmentedControl.Item value="debt">Qarz</SegmentedControl.Item>
</SegmentedControl>
<Button size={size} variant="outline">
Eksport
</Button>
</div>
))}
</div>
);
}
Icons
icon adds an icon before the text. Icon-only items require an aria-label — TypeScript
enforces it.
'use client';
import { SegmentedControl } from '@akhatmirzo/ui';
import { LayoutGrid, List, MapIcon } from 'lucide-react';
export default function SegmentedControlIcons() {
return (
<div className="flex flex-wrap items-center gap-4">
<SegmentedControl defaultValue="grid" aria-label="Ko'rinish">
<SegmentedControl.Item value="grid" icon={<LayoutGrid />} aria-label="Katakli" />
<SegmentedControl.Item value="list" icon={<List />} aria-label="Ro'yxat" />
<SegmentedControl.Item value="map" icon={<MapIcon />} aria-label="Xarita" />
</SegmentedControl>
<SegmentedControl defaultValue="list" aria-label="Ko'rinish">
<SegmentedControl.Item value="grid" icon={<LayoutGrid />}>
Katakli
</SegmentedControl.Item>
<SegmentedControl.Item value="list" icon={<List />}>
Ro'yxat
</SegmentedControl.Item>
</SegmentedControl>
</div>
);
}
Full width
fullWidth fills the container and splits it evenly between items. Handy in mobile forms.
'use client';
import { SegmentedControl } from '@akhatmirzo/ui';
export default function SegmentedControlFullWidth() {
return (
<div className="w-full max-w-sm">
<SegmentedControl fullWidth defaultValue="delivery" aria-label="Olib ketish usuli">
<SegmentedControl.Item value="delivery">Yetkazib berish</SegmentedControl.Item>
<SegmentedControl.Item value="pickup">Olib ketish</SegmentedControl.Item>
</SegmentedControl>
</div>
);
}
Controlled
value + onValueChange — you keep the selection. Clicking the selected item again doesn't clear
it: one value is always selected.
Savdo: 8 740 000 so'm
'use client';
import { SegmentedControl } from '@akhatmirzo/ui';
import { useState } from 'react';
const totals: Record<string, string> = {
day: "1 250 000 so'm",
week: "8 740 000 so'm",
month: "36 900 000 so'm",
};
export default function SegmentedControlControlled() {
const [period, setPeriod] = useState('week');
return (
<div className="flex flex-col items-start gap-3">
<SegmentedControl value={period} onValueChange={setPeriod} aria-label="Davr">
<SegmentedControl.Item value="day">Kun</SegmentedControl.Item>
<SegmentedControl.Item value="week">Hafta</SegmentedControl.Item>
<SegmentedControl.Item value="month">Oy</SegmentedControl.Item>
</SegmentedControl>
<p className="m-0 text-sm text-text-muted">
Savdo: <span className="font-semibold text-text">{totals[period]}</span>
</p>
</div>
);
}
Disabled
Pass disabled to the whole group or to a single SegmentedControl.Item. Disabled items can't be
clicked and the keyboard skips them.
'use client';
import { SegmentedControl } from '@akhatmirzo/ui';
export default function SegmentedControlDisabled() {
return (
<div className="flex flex-col items-start gap-4">
<SegmentedControl defaultValue="card" aria-label="To'lov usuli">
<SegmentedControl.Item value="cash">Naqd</SegmentedControl.Item>
<SegmentedControl.Item value="card">Karta</SegmentedControl.Item>
<SegmentedControl.Item value="credit" disabled>
Nasiya
</SegmentedControl.Item>
</SegmentedControl>
<SegmentedControl defaultValue="uz" disabled aria-label="Til">
<SegmentedControl.Item value="uz">O'zbekcha</SegmentedControl.Item>
<SegmentedControl.Item value="ru">Ruscha</SegmentedControl.Item>
</SegmentedControl>
</div>
);
}
Props
SegmentedControl
Other <div> attributes and Radix RadioGroup props (name, required, loop, dir) are passed
through; ref points to the root element.
| Prop | Type | Default | Description |
|---|---|---|---|
value | string | — | Selected value (controlled mode) |
defaultValue | string | — | Initial value |
onValueChange | (value: string) => void | — | When the selection changes |
size | 'sm' | 'md' | 'lg' | 'md' | Height: 32 / 40 / 48px |
fullWidth | boolean | false | Fills the width, items share it evenly |
disabled | boolean | — | Disables the whole group |
name | string | — | For forms (hidden radio input) |
aria-label | string | — | Group name (e.g. Period or View) |
SegmentedControl.Item
| Prop | Type | Default | Description |
|---|---|---|---|
valuerequired | string | — | Item value |
icon | ReactNode | — | Icon before the text |
disabled | boolean | — | Disables the item |
aria-label | string | — | Required for icon-only items |
Data attributes
| Attribute | Where | Value |
|---|---|---|
data-size | root | sm · md · lg |
data-full-width | root | present with fullWidth |
data-disabled | root, item | present when disabled |
data-state | item | checked · unchecked |
data-slot | indicator | segmented-control-indicator |
Keyboard
| Key | Action |
|---|---|
Tab | Enter the group (on the selected item) and leave it |
← ↑ / → ↓ | Previous / next item — selected as soon as it gets focus |
Accessibility
- Roles. The root is
role="radiogroup", each itemrole="radio"(aria-checked). Built on Radix RadioGroup: single selection with roving focus — the group is oneTabstop. - Name. Give the group an
aria-label("Period"). Inside aFieldthe label names the group and the description is linked througharia-describedby. - Icon-only items need an
aria-label(checked by TypeScript). - The indicator is decorative —
aria-hidden. It measures the selected item and slides over 200ms; it re-measures when sizes change (window width, font loading). Withprefers-reduced-motionit jumps without animating.