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

Source: https://ui.suiss.app/docs/patterns/segment-picker

**React**

**Example**

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

```bash title="Terminal"
npx shadcn@latest add @esuiss/segment-picker
```

Works in any project with a `components.json`. See [Installation](https://ui.suiss.app/docs/installation#add-components) for the one-line registry setup. Or use the suiss/ui CLI:

```bash title="Terminal"
npx @esuiss/ui@latest add patterns segment-picker
```

### Manual

1. Add the pieces it builds on: [Native Select](https://ui.suiss.app/docs/components/native-select), [Toggle Group](https://ui.suiss.app/docs/components/toggle-group), `use-mobile`.

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

```tsx title="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() {
  const context = React.useContext(SegmentPickerContext)

  if (!context) {
    throw new Error("useSegmentPicker must be used within a <SegmentPicker />")
  }

  return context
}

function SegmentPicker({
  value: valueProp,
  defaultValue,
  onValueChange,
  variant = "default",
  size = "default",
  spacing = 0,
  id,
  name,
  disabled,
  className,
  children,
  "aria-label": ariaLabel,
  "aria-describedby": ariaDescribedBy,
}: {
  value?: string
  defaultValue?: string
  onValueChange?: (value: string) => void
  variant?: "default" | "outline"
  size?: "sm" | "default" | "lg"
  spacing?: number
  id?: string
  name?: string
  disabled?: boolean
  className?: string
  children?: React.ReactNode
  "aria-label"?: string
  "aria-describedby"?: string
}) {
  const isMobile = useIsMobile()
  const [uncontrolledValue, setUncontrolledValue] = React.useState(defaultValue)
  const value = valueProp ?? uncontrolledValue

  const select = (next: string) => {
    if (next === value) return
    if (valueProp === undefined) setUncontrolledValue(next)
    onValueChange?.(next)
  }

  return (
    <SegmentPickerContext.Provider value={{ isMobile }}>
      {isMobile ? (
        <NativeSelect
          data-slot="segment-picker"
          id={id}
          name={name}
          size={size === "sm" ? "sm" : "default"}
          disabled={disabled}
          className={className}
          aria-label={ariaLabel}
          aria-describedby={ariaDescribedBy}
          value={value ?? ""}
          onChange={(event) => select(event.target.value)}
        >
          {value === undefined && <NativeSelectOption value="" disabled />}
          {children}
        </NativeSelect>
      ) : (
        <>
          {name && <input type="hidden" name={name} value={value ?? ""} />}
          <ToggleGroup
            data-slot="segment-picker"
            id={id}
            variant={variant}
            size={size}
            spacing={spacing}
            disabled={disabled}
            className={className}
            aria-label={ariaLabel}
            aria-describedby={ariaDescribedBy}
            value={value === undefined ? [] : [value]}
            onValueChange={(next) => {
              const [selected] = next
              if (selected !== undefined) select(selected)
            }}
          >
            {children}
          </ToggleGroup>
        </>
      )}
    </SegmentPickerContext.Provider>
  )
}

function SegmentPickerItem({
  value,
  label,
  disabled,
  className,
  children,
  "aria-label": ariaLabel,
}: {
  value: string
  label?: string
  disabled?: boolean
  className?: string
  children?: React.ReactNode
  "aria-label"?: string
}) {
  const { isMobile } = useSegmentPicker()

  if (isMobile) {
    return (
      <NativeSelectOption
        data-slot="segment-picker-item"
        value={value}
        disabled={disabled}
      >
        {label ??
          ariaLabel ??
          (typeof children === "string" || typeof children === "number"
            ? children
            : value)}
      </NativeSelectOption>
    )
  }

  return (
    <ToggleGroupItem
      data-slot="segment-picker-item"
      value={value}
      disabled={disabled}
      className={className}
      aria-label={ariaLabel}
    >
      {children ?? label}
    </ToggleGroupItem>
  )
}

export { SegmentPicker, SegmentPickerItem }
```

3. Update the import paths to match your project.

## Usage

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

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

**Example**

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

**Example**

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

**Example**

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

**Example**

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

**Example**

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

**Example**

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

**Example**

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

**Example**

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

**Example**

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

- `SegmentPicker`
- `SegmentPickerItem`

### SegmentPicker

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

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `value` | `string` | - | The selected value when controlled. |
| `defaultValue` | `string` | - | 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. |
| `spacing` | `number` | `0` | The gap between desktop segments; `0` joins them. |
| `id` | `string` | - | The ID of the group on desktop or the select on mobile. |
| `name` | `string` | - | The field name submitted with a form. |
| `disabled` | `boolean` | - | Disables every segment. |
| `className` | `string` | - | Classes applied to the group on desktop or the select on mobile. |
| `children` | `React.ReactNode` | - | The `SegmentPickerItem` elements. |
| `aria-label` | `string` | - | The accessible name of the control. |
| `aria-describedby` | `string` | - | The ID of an element that describes the control. |

### SegmentPickerItem

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

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `value` | `string` | Required | The value selected by this segment. |
| `label` | `string` | - | Text for the mobile option, and the desktop content when there are no children. |
| `disabled` | `boolean` | - | Prevents selecting this segment. |
| `className` | `string` | - | Classes applied to the desktop toggle. |
| `children` | `React.ReactNode` | - | The desktop segment content, such as text or an icon. |
| `aria-label` | `string` | - | 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`.

