Navigation
SegmentedControl
An iOS-style segmented control: a row of equal segments with a raised thumb that slides to the checked one, for switching how one view shows its content in place, such as list or grid. Exactly one segment is checked. Screen readers hear a radio group, and the arrow keys check the next segment as they do for radio buttons. Segments are 44px tall on touch screens, and name submits the value with a form.
In Spoonjoy
Spoonjoy, a recipe app, is built with Ouro HX (how).
See it live, no sign-in needed: API playground
In its source: app/components/recipe/RecipeImportPanel.tsx · app/routes/developers.playground.tsx · app/routes/shopping-list.tsx
Install
Once per app, with Tailwind CSS v4 and an @/* path alias (Ouro HX is built on Base UI, so --base base; skip -t in an existing app):
npx shadcn@latest init --base base --preset nova --yes --no-monoreponpx shadcn@latest add ourostack/ouro-ui/ouro-base#v1.2.13Then the component. From v0.8.0 it installs only the helpers it imports and never touches your stylesheet (Installing):
npx shadcn@latest add ourostack/ouro-ui/segmented-control#v1.2.13Or, with the @ouro namespace set up:
npx shadcn@latest add @ouro/ouro-base @ouro/segmented-controlIts tests, stories and Playwright spec are optional. They need Vitest 4.1.11 or later, and from v0.8.0 never change a version your app has (Testing):
npx shadcn@latest add ourostack/ouro-ui/segmented-control#v1.2.13 ourostack/ouro-ui/segmented-control-tests#v1.2.13import { SegmentedControl } from "@/components/ouro/segmented-control/segmented-control";Paths use shadcn's default @/ alias; the CLI rewrites them to yours.
Example
<SegmentedControl
label="View"
hideLabel
items={[
{ value: "list", label: "List" },
{ value: "grid", label: "Grid" },
]}
value={view}
onValueChange={setView}
/>Use instead when
- Use
Tabsinstead when each choice shows different content with its own panel. - Use links in a
<nav>instead when each choice is its own page. - Use a
RadioGroupinstead for a form value that is submitted.
Props
Type: SegmentedControlProps.
className- Type:
string - Layout only: margins, grid placement (
mt-4). Appearance classes warn in development. defaultValue- Type:
string - Default: the first enabled item
- The segment pressed first, when the component keeps its own state.
disabled- Type:
boolean - Disables every segment.
fullWidth- Type:
boolean - Stretches the control to the width of its container, with the segments sharing it equally.
hideLabel- Type:
boolean - Hides the label visually; it still names the control for screen readers. Set it on a view switcher whose segments say what it is.
items(required)- Type:
readonly SegmentedControlItem[] - The segments, in order. Keep to two to five.
label(required)- Type:
string - Names the choice ("View"). Shown above the control unless
hideLabelis set; screen readers always hear it. name- Type:
string - Submits the checked segment's value under this name with the form it sits in, unless the control is
disabled. onValueChange- Type:
((value: string) => void) - Called with the new segment's value when the person presses a segment. Pressing the pressed one does nothing.
ref- Type:
Ref<HTMLDivElement> - Reaches the group element (
role="radiogroup"). value- Type:
string - The pressed segment's value, when you control it. Pair it with
onValueChange.
Don't use
These are not props. Passing one fails the type check with the fix in the error, and in development a console message names the fix.
onChange: Not a prop. Ouro UI usesonValueChange, which gets the value:<SegmentedControl onValueChange={(value) => …}>.options: Not a prop. Ouro UI usesitems:<SegmentedControl items={[{ value, label }]}>.
className is for layout only
Pass margins, width and grid placement (mt-4, w-full, col-span-2). Your classes always win over Ouro UI's: SegmentedControl merges className last with cn (tailwind-merge), so a class that conflicts with its own replaces it.layout-classes
Appearance comes from props, not classes. In development, a class that sets appearance logs one warning per component instance, naming the prop to use instead. A class sets appearance when, after its variants (hover:, sm:) and any ! are removed, it starts with bg-, text-, border-, rounded-, shadow-, font-, ring-, from-, via-, to-, fill- or stroke-, or is border, rounded, shadow or ring, or has a dark: variant. Text wrapping, overflow and table layout classes (text-wrap, text-ellipsis, border-collapse) are layout and do not warn.
Alignment classes (text-center) warn too: set alignment on a parent element.
Types
SegmentedControlItem
One segment.
interface SegmentedControlItem {
/** Identifies the segment: `value`, `defaultValue` and `onValueChange` use it. Unique within the control. */
value: string;
/** The segment's text. Keep it to one short word; every segment gets the width of the longest. */
label: string;
/** Shows the segment but keeps it from being pressed. */
disabled?: boolean | undefined;
/** Not a prop. A segment switches a view in place; to go to another page, use `Tabs` or links in a `<nav>`. */
href?: { 'A segment switches a view in place. To navigate, use links in a <nav>': never };
}Claim sources
- layout-classes: test:registry/ouro/components/ouro/segmented-control/segmented-control.test.tsx::SegmentedControl > lets app layout classes win
verify:testExists(registry/ouro/components/ouro/segmented-control/segmented-control.test.tsx::SegmentedControl > lets app layout classes win)