Segmented control
Segmented control lets learners choose one of two to five options that change how the current view works, such as Course or Research, or Grid or List.
Beta until design adds the control to the Figma design system (today it is the product-level “Learning Tab” on the Design page) and approves the code-only parts: the small size, focus ring, disabled options and full width.
Preview
Change the controls to try every supported combination. The control is controlled here, so the code follows each click.
Selected: all
import { SegmentedControl } from '@nuton/ui'
import { RiCheckboxCircleLine, RiFolder5Line, RiTimeLine } from '@remixicon/react'
const options = [
{ value: 'all', label: 'All', icon: <RiFolder5Line /> },
{ value: 'progress', label: 'In progress', icon: <RiTimeLine /> },
{ value: 'completed', label: 'Completed', icon: <RiCheckboxCircleLine /> },
]
const [filter, setFilter] = useState('all')
<SegmentedControl
aria-label="Course filter"
options={options}
value={filter}
onValueChange={setFilter}
/>Installation
Availability
@nuton/ui and @nuton/tokens are private workspace packages and aren't published yet (D4, D12). Inside the Nuton monorepo, add them as workspace dependencies.
pnpm add @nuton/ui @nuton/tokensnpm install @nuton/ui @nuton/tokensyarn add @nuton/ui @nuton/tokensimport '@nuton/tokens/tokens.css'
import '@nuton/ui/styles.css'Usage
import { SegmentedControl } from '@nuton/ui'
import { RiBookReadLine, RiMicroscopeLine } from '@remixicon/react'
<SegmentedControl
aria-label="Learning mode"
options={[
{ value: 'course', label: 'Course', icon: <RiBookReadLine /> },
{ value: 'research', label: 'Research', icon: <RiMicroscopeLine /> },
]}
value={mode}
onValueChange={setMode}
/>- Use it to choose a value that changes how the current view or the next step works: a mode, a filter, a layout. It is a radio group: one Tab stop, and arrow keys move and select.
- When each option shows its own panel of content, use Tabs. For the same pill look with tab semantics, put
SegmentedTabsListandSegmentedTabsTriggerinsideTabswithTabsContentpanels. - Keep it to 2–5 short options. For more, or for long labels, use a Select or a Radio group.
- Name the group with
aria-labeloraria-labelledby(“Learning mode”, “Library view”). - Options always have a
label. WithiconOnlythe label becomes the accessible name and the tooltip.
Variants and sizes
Two appearances and three sizes. With fullWidth the track fills its container and the options share the spare width.
Appearances
lighter (default) is the shipped Course / Research switch: a white pill on a light track (Figma 3842:31335). filled is the first Learning Tab: a black pill with white text on a stroked track (Figma 211:2887).
<SegmentedControl aria-label="Learning mode" options={modes} defaultValue="course" />
<SegmentedControl aria-label="Learning mode" options={modes} defaultValue="course" appearance="filled" />Sizes
<SegmentedControl aria-label="Mode" options={modes} size="sm" />
<SegmentedControl aria-label="Mode" options={modes} size="md" />
<SegmentedControl aria-label="Mode" options={modes} size="lg" />Icon only
<SegmentedControl
aria-label="Library view"
iconOnly
options={[
{ value: 'grid', label: 'Grid view', icon: <RiLayoutGridLine /> },
{ value: 'list', label: 'List view', icon: <RiListUnordered /> },
]}
/>| Size | Track | Option | Type and icon | Source |
|---|---|---|---|---|
| sm | 32, padding 2 | 28 tall, padding 6 × 10, gap 4 | Label/X Small, 16px icon | Code-only (matches the AI Tutor answer-mode toggle) |
| md | 44, padding 2 | 40 tall, padding 10 × 14, gap 6 | Label/Small, 20px icon | Figma 3842:31335 and the live Course / Research switch |
| lg | 56, padding 6 | 44 tall, padding 12 × 24, gap 4 | Label/Small, 20px icon | Figma 211:2887 |
| Icon only | Same | Square: 28, 40 or 44 | Icon only, label in a tooltip | Figma 403:29188 is 44 × 36; square is code-only |
States and interactions
disabled on the control disables all of them.Motion
The pill slides between options in 200 ms with the standard easing (--nu-duration-base, --nu-ease-standard). With reduced motion it moves instantly. Before the page has run JavaScript, the selected option paints its own pill, so the server render already shows the choice.
State tokens
Set on .nu-segmented.
| Token | lighter | filled |
|---|---|---|
--nu-segmented-bg · -bg-hover | Bg/Weak-50 · Bg/Soft-100 | |
--nu-segmented-border-width | 0 | 1px Stroke/Soft-200, regular-shadow/x-small |
--nu-segmented-pill · -pill-border | Bg/White · Stroke/Soft-200, x-small shadow | Bg/Strong-950 |
--nu-segmented-fg · -fg-hover | Text/Sub-600 · Text/Strong-950 | |
--nu-segmented-fg-selected | Text/Strong-950 | Text/White |
--nu-segmented-fg-disabled | Text/Disabled-500 | |
--nu-segmented-motion | duration-base, ease-standard | |
Composition examples
Course or Research
The live New page switch. The choice changes what Nuton builds next, so it is a value: a radio group.
Library Grid or List
Icon-only, small, beside the course count. The labels name each option and appear as tooltips (the live toggles have no names today).
AI Tutor answer mode
Small, with a description per option. The description is a tooltip and the option's accessible description.
Switching panels: use tabs
“My study materials” and “Learn something new” each show a different panel, so they are tabs. SegmentedTabsList gives Tabs the segmented look; the hidden panel is removed, so nothing invisible stays focusable (a live-app issue today).
API reference
SegmentedControl props
| Prop | Type | Default | Description |
|---|---|---|---|
options | SegmentedControlOption[] | — | 2–5 options: { value, label, icon?, description?, disabled? }. label is always required. |
value · defaultValue | string | — | Controlled or uncontrolled value. Default: the first enabled option. |
onValueChange | (value) => void | — | Called with the new value. Not called when the selected option is chosen again. |
size | 'sm' | 'md' | 'lg' | 'md' | Track height 32, 44 or 56. |
appearance | 'lighter' | 'filled' | 'lighter' | White pill on a light track, or black pill on a stroked track. |
fullWidth | boolean | false | Fills the container; options share the spare width. Labels are cut with an ellipsis only when they no longer fit. |
iconOnly | boolean | false | Shows icons only. Labels stay as accessible names and tooltips. Every option needs an icon. |
disabled | boolean | false | Disables every option. |
name | string | — | Submits the value with a form (a hidden input). |
dir | 'ltr' | 'rtl' | — | Reading direction for arrow keys. Default: read from the page. |
aria-label · aria-labelledby | string | — | Names the group. Required (a development warning shows if missing). |
…div props | HTMLAttributes<HTMLDivElement> | — | className, id, data attributes and handlers go to the radiogroup element. The ref points at it. |
SegmentedTabsList and SegmentedTabsTrigger props
| Prop | Type | Default | Description |
|---|---|---|---|
SegmentedTabsList: size · appearance · fullWidth · iconOnly | as above | — | The same look. Use inside Tabs; Radix TabsList props (loop, aria-label) pass through. |
SegmentedTabsTrigger: value | string | — | The panel this tab shows (matches a TabsContent value). |
SegmentedTabsTrigger: children | string | — | The label. A string, so it can also be the tooltip when icon-only. |
SegmentedTabsTrigger: icon · iconOnly · disabled | ReactNode · boolean · boolean | — | Decorative icon; hide the label visually (set it to match the list); disable this tab. |
Accessibility
Keyboard
| Key | Behaviour |
|---|---|
| Tab | Moves focus to the selected option (one tab stop for the whole control) |
| → / ↓ | Selects the next enabled option (wraps). Reversed for ← in right-to-left pages |
| ← / ↑ | Selects the previous enabled option (wraps) |
| Home / End | Selects the first or last enabled option |
| Space | Selects the focused option |
Screen readers
role="radiogroup" named by your label, with role="radio" options and aria-checked: “Research, radio button, 2 of 2, not checked”. Icons and the sliding pill are hidden. Descriptions are linked with aria-describedby. The tabs version uses the tabs pattern instead.
Focus
A roving tabindex keeps one stop. The 2px ring uses the focus-ring tokens (code-only; Figma has no focus state).
Contrast and colour
Unselected text Sub-600 is 6.24:1 on Weak-50 and 6.13:1 on the Soft-100 hover track; selected Strong-950 on white 17.93:1, White on Strong-950 17.93:1. The selected option also differs by the pill and its shadow, so the choice never relies on colour alone. Forced colours use Highlight for the pill.
Touch and pointer
Options are at least 28 × 28 (sm) and 40 tall at md, above the 24 × 24 minimum (WCAG 2.5.8).
You must: name the group, give every option a label (even icon-only), and use Tabs, not this control, when options switch panels.
Figma mapping
| Figma | Values | Code |
|---|---|---|
| Learning Tab 211:2887, Property 1 | Study Materials · Learn New | appearance="filled" size="lg", the selected value |
| Learning Tab 403:29188 | Icon only | appearance="filled" iconOnly |
| Learning Tab 3842:31335, Property 1 × State | Learn New · Research × Default · Hover | appearance="lighter" (default), md, hover state |
| (none) | — | sm size, focus, disabled options, full width and descriptions are code-only |
Guidelines and related components
Use Segmented control for
- A mode for what happens next (Course or Research)
- A layout of the same content (Grid or List)
- A quick filter with 2–5 short options
Don't use it for
- Showing different panels of content (use Tabs or SegmentedTabsList)
- Actions such as Save or Share (use Button group)
- More than five options or long labels (use Select or Radio group)
- A single on/off setting (use Switch)