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
  • Accessibility
  • Figma Learning Tab 211:2887 · 403:29188 · 3842:31335
  • packages/ui/src/components/SegmentedControl

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

tsx
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.

bash
pnpm add @nuton/ui @nuton/tokens
tsx
import '@nuton/tokens/tokens.css'
import '@nuton/ui/styles.css'

Usage

tsx
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 SegmentedTabsList and SegmentedTabsTrigger inside Tabs with TabsContent panels.
  • 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-label or aria-labelledby (“Learning mode”, “Library view”).
  • Options always have a label. With iconOnly the 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).

Sizes

sm · 32
md · 44
lg · 56

Icon only

SizeTrackOptionType and iconSource
sm32, padding 228 tall, padding 6 × 10, gap 4Label/X Small, 16px iconCode-only (matches the AI Tutor answer-mode toggle)
md44, padding 240 tall, padding 10 × 14, gap 6Label/Small, 20px iconFigma 3842:31335 and the live Course / Research switch
lg56, padding 644 tall, padding 12 × 24, gap 4Label/Small, 20px iconFigma 211:2887
Icon onlySameSquare: 28, 40 or 44Icon only, label in a tooltipFigma 403:29188 is 44 × 36; square is code-only

States and interactions

SelectedA pill slides behind the chosen option. Text Strong-950 (lighter) or White (filled).
HoverThe track turns Bg/Soft-100 (Figma 3842:31335) and the option text turns Strong-950 (live app).
Focus (code-only)Figma has none. A 2px State/Primary/Base ring (the focus-ring tokens). Press Tab to see it.
Disabled option (code-only)Text/Disabled-500, skipped by arrow keys. 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.

Tokenlighterfilled
--nu-segmented-bg · -bg-hoverBg/Weak-50 · Bg/Soft-100
--nu-segmented-border-width01px Stroke/Soft-200, regular-shadow/x-small
--nu-segmented-pill · -pill-borderBg/White · Stroke/Soft-200, x-small shadowBg/Strong-950
--nu-segmented-fg · -fg-hoverText/Sub-600 · Text/Strong-950
--nu-segmented-fg-selectedText/Strong-950Text/White
--nu-segmented-fg-disabledText/Disabled-500
--nu-segmented-motionduration-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.

Nuton builds a structured course with lessons and quizzes.

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).

11 courses

Courses are shown as a grid.

AI Tutor answer mode

Small, with a description per option. The description is a tooltip and the option's accessible description.

Answer mode: grounded

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).

Add a document and we'll help you take it one step at a time.

API reference

SegmentedControl props

SegmentedControl props
PropTypeDefaultDescription
optionsSegmentedControlOption[]—2–5 options: { value, label, icon?, description?, disabled? }. label is always required.
value · defaultValuestring—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.
fullWidthbooleanfalseFills the container; options share the spare width. Labels are cut with an ellipsis only when they no longer fit.
iconOnlybooleanfalseShows icons only. Labels stay as accessible names and tooltips. Every option needs an icon.
disabledbooleanfalseDisables every option.
namestring—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-labelledbystring—Names the group. Required (a development warning shows if missing).
…div propsHTMLAttributes<HTMLDivElement>—className, id, data attributes and handlers go to the radiogroup element. The ref points at it.

SegmentedTabsList and SegmentedTabsTrigger props

SegmentedTabsList and SegmentedTabsTrigger props
PropTypeDefaultDescription
SegmentedTabsList: size · appearance · fullWidth · iconOnlyas above—The same look. Use inside Tabs; Radix TabsList props (loop, aria-label) pass through.
SegmentedTabsTrigger: valuestring—The panel this tab shows (matches a TabsContent value).
SegmentedTabsTrigger: childrenstring—The label. A string, so it can also be the tooltip when icon-only.
SegmentedTabsTrigger: icon · iconOnly · disabledReactNode · boolean · boolean—Decorative icon; hide the label visually (set it to match the list); disable this tab.

Accessibility

Keyboard

KeyBehaviour
TabMoves 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 / EndSelects the first or last enabled option
SpaceSelects 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

FigmaValuesCode
Learning Tab 211:2887, Property 1Study Materials · Learn Newappearance="filled" size="lg", the selected value
Learning Tab 403:29188Icon onlyappearance="filled" iconOnly
Learning Tab 3842:31335, Property 1 × StateLearn New · Research × Default · Hoverappearance="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)

Do and don't

DoKeep labels short and parallel Options read as a set and fit on a phone.
Don'tUse it as a row of actions A segmented control keeps a selection. Actions don't stay selected; use a Button group.
DoName icon-only options The label is the accessible name and the tooltip, so everyone knows what each icon does.
Don'tUse long or uneven labels Uneven options look unbalanced, are harder to scan and get cut off on phones.