suiss/ui
  • Docs
  • Components
  • Patterns
  • Interactions
  • Charts
  • Blocks
GitHubGet started
DocsComponentsPatternsInteractionsChartsBlocks
Patterns19
  • Overview
  • Action Bar
  • Action Menu
  • App Shell
  • Command Palette
  • Confirm
  • Context Actions
  • Date Picker
  • Flyout
  • Modal
  • Navbar
  • Paged List
  • Panel
  • Picker
  • Preview
  • Search Picker
  • Segment Picker
  • Split View
  • Table List
  • Trail

suiss/ui · Pattern

Segment Picker

A segmented control for one exclusive choice that becomes a native select on mobile.

Segment Picker is an open-source pattern from suiss/ui for React. The @esuiss/ui CLI copies its source into your project, so you own the code.

import {
  SegmentPicker,
  SegmentPickerItem,
} from "@/components/patterns/segment-picker"

const periods = (
  <>
    <SegmentPickerItem value="day">Day</SegmentPickerItem>
    <SegmentPickerItem value="week">Week</SegmentPickerItem>
    <SegmentPickerItem value="month">Month</SegmentPickerItem>
    <SegmentPickerItem value="year">Year</SegmentPickerItem>
  </>
)

export default function SegmentPickerDemo() {
  return (
    <SegmentPicker defaultValue="week" aria-label="Period">
      {periods}
    </SegmentPicker>
  )
}

SegmentPicker presents a small set of mutually exclusive options, such as a time period or a view mode. On desktop it renders a suiss/ui ToggleGroup in which exactly one segment stays pressed; clicking the pressed segment keeps it selected. On mobile (below 48rem, via useIsMobile) it renders a NativeSelect so the choice fits narrow screens and uses the platform picker.

Each SegmentPickerItem becomes a toggle on desktop and an option on mobile. Use it for two to five short options that change how content is displayed.

Installation

CLI

$ pnpm dlx shadcn@latest add @esuiss/segment-picker

Works in any project with a components.json. See Installation for the one-line registry setup. Or use the suiss/ui CLI:

$ pnpm dlx @esuiss/ui@latest add patterns segment-picker

Manual

  1. Add the pieces it builds on: Native Select, Toggle Group, use-mobile.

  2. Copy the source into components/patterns/segment-picker.tsx:

components/patterns/segment-picker.tsx
"use client"

import * as React from "react"

import { NativeSelect, NativeSelectOption } from "@/components/ui/native-select"
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"
import { useIsMobile } from "@/hooks/use-mobile"

type SegmentPickerContextProps = {
  isMobile: boolean
}

const SegmentPickerContext =
  React.createContext<SegmentPickerContextProps | null>(null)

function useSegmentPicker() {
  1. Update the import paths to match your project.

Usage

import { SegmentPicker, SegmentPickerItem } from "@/components/patterns/segment-picker"
<SegmentPicker defaultValue="week" aria-label="Period">{periods}</SegmentPicker>
  • The value is uncontrolled by default; use defaultValue, or value with onValueChange to control it. onValueChange fires only when the selection changes.
  • Without a value, no segment is pressed and the mobile select shows an empty disabled option.
  • On mobile each option shows label, then aria-label, then string children, then value. Give icon-only segments an aria-label, and segments with mixed content a label.
  • variant and spacing apply only on desktop. On mobile, lg falls back to the default size.
  • With name, desktop renders a hidden input so the value is submitted with a form.

Examples

Without value

Starts with no segment pressed.

import {
  SegmentPicker,
  SegmentPickerItem,
} from "@/components/patterns/segment-picker"

const periods = (
  <>
    <SegmentPickerItem value="day">Day</SegmentPickerItem>
    <SegmentPickerItem value="week">Week</SegmentPickerItem>
    <SegmentPickerItem value="month">Month</SegmentPickerItem>
    <SegmentPickerItem value="year">Year</SegmentPickerItem>
  </>
)

export default function SegmentPickerWithoutValue() {
  return (
    <SegmentPicker defaultValue={undefined} aria-label="Period">
      {periods}
    </SegmentPicker>
  )
}

Outline

Uses the outline variant.

import {
  SegmentPicker,
  SegmentPickerItem,
} from "@/components/patterns/segment-picker"

const periods = (
  <>
    <SegmentPickerItem value="day">Day</SegmentPickerItem>
    <SegmentPickerItem value="week">Week</SegmentPickerItem>
    <SegmentPickerItem value="month">Month</SegmentPickerItem>
    <SegmentPickerItem value="year">Year</SegmentPickerItem>
  </>
)

export default function SegmentPickerOutline() {
  return (
    <SegmentPicker defaultValue="week" aria-label="Period" variant="outline">
      {periods}
    </SegmentPicker>
  )
}

Spaced

Separates the segments with spacing.

import {
  SegmentPicker,
  SegmentPickerItem,
} from "@/components/patterns/segment-picker"

const periods = (
  <>
    <SegmentPickerItem value="day">Day</SegmentPickerItem>
    <SegmentPickerItem value="week">Week</SegmentPickerItem>
    <SegmentPickerItem value="month">Month</SegmentPickerItem>
    <SegmentPickerItem value="year">Year</SegmentPickerItem>
  </>
)

export default function SegmentPickerSpaced() {
  return (
    <SegmentPicker defaultValue="week" aria-label="Period" spacing={2}>
      {periods}
    </SegmentPicker>
  )
}

Sizes

Shows the sm, default and lg sizes.

import {
  SegmentPicker,
  SegmentPickerItem,
} from "@/components/patterns/segment-picker"

const periods = (
  <>
    <SegmentPickerItem value="day">Day</SegmentPickerItem>
    <SegmentPickerItem value="week">Week</SegmentPickerItem>
    <SegmentPickerItem value="month">Month</SegmentPickerItem>
    <SegmentPickerItem value="year">Year</SegmentPickerItem>
  </>
)

export default function SegmentPickerSizes() {
  return (
    <div className="flex flex-col items-start gap-3">
      {(["sm", "default", "lg"] as const).map((size) => (
        <SegmentPicker
          key={size}
          defaultValue="week"
          aria-label="Period"
          size={size}
        >
          {periods}
        </SegmentPicker>
      ))}
    </div>
  )
}

With icons

Uses icon-only segments named by aria-label.

import { ListIcon, SquaresFourIcon, TableIcon } from "@phosphor-icons/react"
import {
  SegmentPicker,
  SegmentPickerItem,
} from "@/components/patterns/segment-picker"

export default function SegmentPickerWithIcons() {
  return (
    <SegmentPicker defaultValue="grid" aria-label="View">
      <SegmentPickerItem value="list" aria-label="List">
        <ListIcon />
      </SegmentPickerItem>
      <SegmentPickerItem value="grid" aria-label="Grid">
        <SquaresFourIcon />
      </SegmentPickerItem>
      <SegmentPickerItem value="table" aria-label="Table">
        <TableIcon />
      </SegmentPickerItem>
    </SegmentPicker>
  )
}

With icons and text

Combines icons and text, with label for the mobile option text.

import { ListIcon, SquaresFourIcon, TableIcon } from "@phosphor-icons/react"
import {
  SegmentPicker,
  SegmentPickerItem,
} from "@/components/patterns/segment-picker"

export default function SegmentPickerWithIconsAndText() {
  return (
    <SegmentPicker defaultValue="grid" aria-label="View">
      <SegmentPickerItem value="list" label="List">
        <ListIcon data-icon="inline-start" />
        List
      </SegmentPickerItem>
      <SegmentPickerItem value="grid" label="Grid">
        <SquaresFourIcon data-icon="inline-start" />
        Grid
      </SegmentPickerItem>
      <SegmentPickerItem value="table" label="Table">
        <TableIcon data-icon="inline-start" />
        Table
      </SegmentPickerItem>
    </SegmentPicker>
  )
}

Disabled item

Disables a single segment.

import {
  SegmentPicker,
  SegmentPickerItem,
} from "@/components/patterns/segment-picker"

export default function SegmentPickerDisabledItem() {
  return (
    <SegmentPicker defaultValue="week" aria-label="Period">
      <SegmentPickerItem value="day">Day</SegmentPickerItem>
      <SegmentPickerItem value="week">Week</SegmentPickerItem>
      <SegmentPickerItem value="month">Month</SegmentPickerItem>
      <SegmentPickerItem value="year" disabled>
        Year
      </SegmentPickerItem>
    </SegmentPicker>
  )
}

Disabled

Disables every segment.

import {
  SegmentPicker,
  SegmentPickerItem,
} from "@/components/patterns/segment-picker"

const periods = (
  <>
    <SegmentPickerItem value="day">Day</SegmentPickerItem>
    <SegmentPickerItem value="week">Week</SegmentPickerItem>
    <SegmentPickerItem value="month">Month</SegmentPickerItem>
    <SegmentPickerItem value="year">Year</SegmentPickerItem>
  </>
)

export default function SegmentPickerDisabled() {
  return (
    <SegmentPicker defaultValue="week" aria-label="Period" disabled>
      {periods}
    </SegmentPicker>
  )
}

Controlled

Owns the value in state with value and onValueChange.

import * as React from "react"
import {
  SegmentPicker,
  SegmentPickerItem,
} from "@/components/patterns/segment-picker"

const periods = (
  <>
    <SegmentPickerItem value="day">Day</SegmentPickerItem>
    <SegmentPickerItem value="week">Week</SegmentPickerItem>
    <SegmentPickerItem value="month">Month</SegmentPickerItem>
    <SegmentPickerItem value="year">Year</SegmentPickerItem>
  </>
)

function ControlledExample(props: React.ComponentProps<typeof SegmentPicker>) {
  const [value, setValue] = React.useState("month")

  return (
    <div className="flex flex-col items-start gap-2">
      <SegmentPicker {...props} value={value} onValueChange={setValue}>
        {periods}
      </SegmentPicker>
      <p className="text-sm text-muted-foreground">Selected: {value}</p>
    </div>
  )
}

export default function SegmentPickerControlled() {
  return <ControlledExample defaultValue="week" aria-label="Period" />
}

API Reference

Anatomy2 parts
<SegmentPicker>
<SegmentPickerItem />
</SegmentPicker>
01

SegmentPicker

The root that renders the toggle group or native select and holds the value.

PropTypeDefaultDescription
valuestring-The selected value when controlled.
defaultValuestring-The initially selected value when uncontrolled.
onValueChange(value: string) => void-Called with the new value when the user selects a different segment.
variant"default" | "outline""default"The visual style of the desktop segments.
size"sm" | "default" | "lg""default"The control size; lg maps to the default size on mobile.
spacingnumber0The gap between desktop segments; 0 joins them.
idstring-The ID of the group on desktop or the select on mobile.
namestring-The field name submitted with a form.
disabledboolean-Disables every segment.
classNamestring-Classes applied to the group on desktop or the select on mobile.
childrenReact.ReactNode-The SegmentPickerItem elements.
aria-labelstring-The accessible name of the control.
aria-describedbystring-The ID of an element that describes the control.
02

SegmentPickerItem

One option, rendered as a toggle on desktop and an option on mobile.

PropTypeDefaultDescription
valuestringRequiredThe value selected by this segment.
labelstring-Text for the mobile option, and the desktop content when there are no children.
disabledboolean-Prevents selecting this segment.
classNamestring-Classes applied to the desktop toggle.
childrenReact.ReactNode-The desktop segment content, such as text or an icon.
aria-labelstring-The accessible name of the segment, also used as mobile option text.

Accessibility

  • On desktop the root is a group named by aria-label, and each segment is a button with aria-pressed.
  • Disabled segments are disabled buttons and cannot be selected.
  • On mobile the control is a native select.

Notes

Styling hooks: data-slot="segment-picker" and segment-picker-item. SegmentPickerItem throws when used outside SegmentPicker.

NextSplit ViewPreviousSearch Picker

On this page

  1. 01Installation
    • CLI
    • Manual
  2. 02Usage
  3. 03Examples
    • Without value
    • Outline
    • Spaced
    • Sizes
    • With icons
    • With icons and text
    • Disabled item
    • Disabled
    • Controlled
  4. 04API Reference
    • SegmentPicker
    • SegmentPickerItem
  5. 05Accessibility
  6. 06Notes