FairGarden Design

Toggle

A Base UI Toggle for immediate, reversible changes to how content is shown, and the Toggle Group that joins toggles into a segmented control or a set of filter chips.

import { Toggle } from '@fairgarden-private/design/components/Toggle'
import { ToggleGroup } from '@fairgarden-private/design/components/ToggleGroup'

Use toggles for view modes, units, formatting, play and pause, and filters. Values a form submits use Radio or Checkbox Group, and panels use Tabs. Labels are authored in title case. Selection is always a 3 px inside edge, a glyph and weight 700; any fill is extra.

Toggles and states

Toggles and states

The text toggle looks like an outline Button; pressed, it takes the inverse pair and a leading ✓. Hover takes a soft --primary3 fill (on the pastels, forest and the solid fields, where soft fills drop, the label takes the accent underline instead); a pressed toggle's fill lightens to --primary11 on hover, and pressing shows the inverse pair for a moment. The icon toggle's glyph takes its heavier weight on hover. The icon toggle is a circle whose icon swaps when pressed (iconpressedIcon, ▷ ↔ ‖), never only a filled twin of the same glyph; its label is the visually hidden accessible name. sm is 32 px with its hit area extended to 44. Disabled draws a dotted edge; a selected item is never disabled.

Paused
ToggleStates.tsx
'use client'

import * as React from 'react'
import { Toggle } from '@fairgarden-private/design/components/Toggle'
import styles from './states.module.css'

/**
 * Text toggles (pressed shows ✓, the inverse pair and weight 700), an icon
 * toggle that swaps ▷ for ‖, sizes, and a disabled toggle.
 */
export function ToggleStates() {
  const [playing, setPlaying] = React.useState(false)

  return (
    <div className={styles.stack}>
      <div className={styles.row}>
        <Toggle defaultPressed>Show Trails</Toggle>
        <Toggle icon="zoom_in">Magnify</Toggle>
        <Toggle size="sm">Small</Toggle>
        <Toggle disabled>Offline Maps</Toggle>
      </div>
      <div className={styles.row}>
        <Toggle
          iconOnly
          icon="play_arrow"
          pressedIcon="pause"
          pressed={playing}
          onPressedChange={setPlaying}
        >
          {playing ? 'Pause Birdsong' : 'Play Birdsong'}
        </Toggle>
        <span className={styles.status} aria-live="polite">
          {playing ? 'Playing' : 'Paused'}
        </span>
      </div>
    </div>
  )
}

Segmented control and filter chips

Segmented control and filter chips

variant="segmented" joins 2–4 cells in one 2 px outline with --role-rule dividers, hidden beside the pressed cell; cells are equal at base and hug their labels from 768 px. More than four options belong in a Select. variant="chip" makes filter chips that wrap and never scroll: a quiet 1.5 px --role-rule edge at rest that turns --primary12 at the same weight on hover, with the soft fill where it applies, and --role-select when pressed (--role-select-hover on hover). A single-choice group marks the pressed item with ●, a multiple group with ✓. label renders a visible group label, which may echo the value ("Size: M").

View
Filter Trails
Size: M
Units
ToggleGroups.tsx
'use client'

import * as React from 'react'
import { Toggle } from '@fairgarden-private/design/components/Toggle'
import { ToggleGroup } from '@fairgarden-private/design/components/ToggleGroup'
import styles from './groups.module.css'

const sizes = ['XS', 'S', 'M', 'L', 'XL']

/**
 * A segmented view switch (single: ●), filter chips (multiple: ✓) and a
 * single-choice chip group whose label echoes the value.
 */
export function ToggleGroups() {
  const [size, setSize] = React.useState<string[]>(['M'])

  return (
    <div className={styles.stack}>
      <ToggleGroup variant="segmented" label="View" defaultValue={['map']}>
        <Toggle value="map">Map</Toggle>
        <Toggle value="list">List</Toggle>
        <Toggle value="grid">Grid</Toggle>
      </ToggleGroup>
      <ToggleGroup variant="chip" label="Filter Trails" multiple defaultValue={['shaded']}>
        <Toggle value="shaded">Shaded</Toggle>
        <Toggle value="loop">Loop Trail</Toggle>
        <Toggle value="dogs">Dog Friendly</Toggle>
        <Toggle value="water">Near Water</Toggle>
        <Toggle value="access" disabled>
          Step Free
        </Toggle>
      </ToggleGroup>
      <ToggleGroup
        variant="chip"
        label={`Size: ${size[0] ?? 'None'}`}
        value={size}
        onValueChange={setSize}
      >
        {sizes.map((value) => (
          <Toggle key={value} value={value}>
            {value}
          </Toggle>
        ))}
      </ToggleGroup>
      <ToggleGroup label="Units" defaultValue={['metric']}>
        <Toggle value="metric">Metric</Toggle>
        <Toggle value="imperial">Imperial</Toggle>
      </ToggleGroup>
    </div>
  )
}

Primary and secondary

Primary and secondary

Toggles and segments draw every part from primary. secondary drives only the selected filter chip's fill, mark and edge. Neither is defaulted.

ToggleColor.tsx
import { Toggle } from '@fairgarden-private/design/components/Toggle'
import { ToggleGroup } from '@fairgarden-private/design/components/ToggleGroup'
import styles from './color.module.css'

/**
 * Toggles and segments draw every part from `primary`; `secondary` drives
 * only the selected filter chip.
 */
export function ToggleColor() {
  return (
    <div className={styles.grid}>
      <Toggle defaultPressed primary="plum">
        Primary Plum
      </Toggle>
      <ToggleGroup variant="chip" multiple aria-label="Scope chips" defaultValue={['a']}>
        <Toggle value="a">Scope Chip</Toggle>
      </ToggleGroup>
      <ToggleGroup
        variant="chip"
        multiple
        secondary="indigo"
        aria-label="Indigo chips"
        defaultValue={['a']}
      >
        <Toggle value="a">Secondary Indigo</Toggle>
      </ToggleGroup>
    </div>
  )
}

On grounds

On paper and forest

Toggles and segments select by the inverse pair on every ground. Selected chips are green on paper and the inverse pair on forest and the saturated grounds. Toggles don't print; print the result as a caption ("View: Week").

paper
forest
ToggleGrounds.tsx
import { Toggle } from '@fairgarden-private/design/components/Toggle'
import { ToggleGroup } from '@fairgarden-private/design/components/ToggleGroup'
import { PresetGround } from '@/components/PresetGround'
import styles from './grounds.module.css'

const presets = ['paper', 'forest'] as const

/**
 * Segments take the inverse pair on every ground; the selected chip is
 * green on paper and the inverse pair on forest.
 */
export function ToggleGrounds() {
  return (
    <div className={styles.row}>
      {presets.map((preset) => (
        <PresetGround key={preset} preset={preset} className={styles.face}>
          <span className={styles.name}>{preset}</span>
          <ToggleGroup variant="segmented" aria-label={`View on ${preset}`} defaultValue={['day']}>
            <Toggle value="day">Day</Toggle>
            <Toggle value="week">Week</Toggle>
          </ToggleGroup>
          <ToggleGroup
            variant="chip"
            multiple
            aria-label={`Filters on ${preset}`}
            defaultValue={['birds']}
          >
            <Toggle value="birds">Birds</Toggle>
            <Toggle value="ferns">Ferns</Toggle>
          </ToggleGroup>
        </PresetGround>
      ))}
    </div>
  )
}

API Reference

Toggle

A Base UI Toggle: an on/off button for immediate, reversible changes to how content is shown (view, units, formatting, filters). Selection is a heavier inside edge, a glyph and a weight change; fill is extra [D15]. Values a form submits use Radio or Checkbox Group; panels use Tabs.

PropTypeDescription
icon
| 'menu'
| 'search'
| 'circle'
| 'arrow_forward'
| 'arrow_upward'
| 'expand_more'
| 'close'
| 'remove'
| 'add'
| 'check'
| 'chevron_right'
| 'chevron_left'
| 'more_horiz'
| 'play_arrow'
| 'pause'
| 'download'
| 'zoom_in'
| 'zoom_out'
| 'recenter'
| 'help'
| 'mail'
| undefined

One optional leading glyph (§6.10), inline tier, FILL 0.

iconOnly
boolean | null | undefined

true shows only the icon; see the icon toggle. Default false.

pressedIcon
| 'menu'
| 'search'
| 'circle'
| 'arrow_forward'
| 'arrow_upward'
| 'expand_more'
| 'close'
| 'remove'
| 'add'
| 'check'
| 'chevron_right'
| 'chevron_left'
| 'more_horiz'
| 'play_arrow'
| 'pause'
| 'download'
| 'zoom_in'
| 'zoom_out'
| 'recenter'
| 'help'
| 'mail'
| undefined

Not used on a labelled toggle: it shows ✓ or ● when pressed.

primary
| 'ruby'
| 'olive'
| 'sage'
| 'slate'
| 'sand'
| 'gray'
| 'mauve'
| 'brown'
| 'bronze'
| 'gold'
| 'red'
| 'crimson'
| 'tomato'
| 'pink'
| 'plum'
| 'indigo'
| 'iris'
| 'violet'
| 'purple'
| null
| undefined

Primary Radix scale: edges, label, the inverse pair and the ring. Never defaulted [D133].

secondary
| 'ruby'
| 'olive'
| 'sage'
| 'slate'
| 'sand'
| 'gray'
| 'mauve'
| 'brown'
| 'bronze'
| 'gold'
| 'red'
| 'crimson'
| 'tomato'
| 'pink'
| 'plum'
| 'indigo'
| 'iris'
| 'violet'
| 'purple'
| 'amber'
| 'blue'
| 'cyan'
| 'grass'
| 'green'
| 'jade'
| 'lime'
| 'mint'
| 'orange'
| 'sky'
| 'teal'
| 'yellow'
| null
| undefined

Secondary Radix scale: drives only the selected chip (--role-select). Never defaulted.

size
'sm' | 'md' | null | undefined

Height: sm 32 px (chips, toolbars; hit area extended to 44) or md 40 px (segments, standalone; default, chips default to sm).

variant
'outline' | 'segment' | 'chip' | null | undefined

outline (default): a pill like the outline Button, selected by the inverse pair. segment: a joined cell of a segmented group. chip: a filter chip, selected by --role-select. Inside a ToggleGroup the group sets it.

children
React.ReactNode | undefined

The label, authored in title case (“Grid View”) [D160].

ToggleGroup

A Base UI Toggle Group of Toggles, single (default) or multiple. The group passes segment or chip to its Toggles and tells them whether the pressed mark is ● (single) or ✓ (multiple). Use it for immediate, reversible view changes and filters, never to switch panels (Tabs). The outline and chip roots are inline-size containers, so give them a width in shrink-to-fit layouts; the segmented root sizes to its cells.

PropTypeDescription
label
React.ReactNode | undefined

A visible group label (type-field-label, title case), rendered before the group and wired with aria-labelledby. It may echo the value (“Size: M”). Omit it and pass aria-label or aria-labelledby instead where the label lives elsewhere.

primary
| 'ruby'
| 'olive'
| 'sage'
| 'slate'
| 'sand'
| 'gray'
| 'mauve'
| 'brown'
| 'bronze'
| 'gold'
| 'red'
| 'crimson'
| 'tomato'
| 'pink'
| 'plum'
| 'indigo'
| 'iris'
| 'violet'
| 'purple'
| null
| undefined

Primary Radix scale for the group and its toggles. Never defaulted [D133].

secondary
| 'ruby'
| 'olive'
| 'sage'
| 'slate'
| 'sand'
| 'gray'
| 'mauve'
| 'brown'
| 'bronze'
| 'gold'
| 'red'
| 'crimson'
| 'tomato'
| 'pink'
| 'plum'
| 'indigo'
| 'iris'
| 'violet'
| 'purple'
| 'amber'
| 'blue'
| 'cyan'
| 'grass'
| 'green'
| 'jade'
| 'lime'
| 'mint'
| 'orange'
| 'sky'
| 'teal'
| 'yellow'
| null
| undefined

Secondary Radix scale: the selected chips’ --role-select. Never defaulted.

variant
'outline' | 'chip' | 'segmented' | null | undefined

outline (default): separate text toggles. segmented: joined cells in one --border-size-2 container, for 2–4 options (4+ belong in a Select). chip: filter chips that wrap and never scroll.

Additional types

toggle
type toggle = toggle
toggleGroup
ToggleItemContext

Provided by ToggleGroup; a standalone Toggle reads undefined.

type ToggleItemContext = React.Context<ToggleItemContextValue | undefined>
ToggleItemContextValue

What a ToggleGroup tells the toggles inside it.

type ToggleItemContextValue = {
  /** The item look the group's variant implies. */
  variant: ToggleVariant;
  /** Whether the group allows several pressed items: ✓ rather than ●. */
  multiple: boolean;
}
ToggleProps

Props for Toggle: Base UI Toggle props plus the variant, size and color axes. With iconOnly, icon, pressedIcon and an accessible-name children are required.

type ToggleProps = (
  | {
      iconOnly?: false | null;
      icon?:
        | 'menu'
        | 'search'
        | 'circle'
        | 'arrow_forward'
        | 'arrow_upward'
        | 'expand_more'
        | 'close'
        | 'remove'
        | 'add'
        | 'check'
        | 'chevron_right'
        | 'chevron_left'
        | 'more_horiz'
        | 'play_arrow'
        | 'pause'
        | 'download'
        | 'zoom_in'
        | 'zoom_out'
        | 'recenter'
        | 'help'
        | 'mail';
      pressedIcon?: undefined;
      children?: React.ReactNode;
    }
  | {
      iconOnly: true;
      icon:
        | 'menu'
        | 'search'
        | 'circle'
        | 'arrow_forward'
        | 'arrow_upward'
        | 'expand_more'
        | 'close'
        | 'remove'
        | 'add'
        | 'check'
        | 'chevron_right'
        | 'chevron_left'
        | 'more_horiz'
        | 'play_arrow'
        | 'pause'
        | 'download'
        | 'zoom_in'
        | 'zoom_out'
        | 'recenter'
        | 'help'
        | 'mail';
      pressedIcon:
        | 'menu'
        | 'search'
        | 'circle'
        | 'arrow_forward'
        | 'arrow_upward'
        | 'expand_more'
        | 'close'
        | 'remove'
        | 'add'
        | 'check'
        | 'chevron_right'
        | 'chevron_left'
        | 'more_horiz'
        | 'play_arrow'
        | 'pause'
        | 'download'
        | 'zoom_in'
        | 'zoom_out'
        | 'recenter'
        | 'help'
        | 'mail';
      children: React.ReactNode;
    }
) & {
  /**
   * `outline` (default): a pill like the outline Button, selected by the
   * inverse pair. `segment`: a joined cell of a segmented group. `chip`: a
   * filter chip, selected by --role-select. Inside a ToggleGroup the group
   * sets it.
   */
  variant?: 'outline' | 'segment' | 'chip' | null;
  /** Primary Radix scale: edges, label, the inverse pair and the ring. Never defaulted [D133]. */
  primary?:
    | 'ruby'
    | 'olive'
    | 'sage'
    | 'slate'
    | 'sand'
    | 'gray'
    | 'mauve'
    | 'brown'
    | 'bronze'
    | 'gold'
    | 'red'
    | 'crimson'
    | 'tomato'
    | 'pink'
    | 'plum'
    | 'indigo'
    | 'iris'
    | 'violet'
    | 'purple'
    | null;
  /** Secondary Radix scale: drives only the selected chip (--role-select). Never defaulted. */
  secondary?:
    | 'ruby'
    | 'olive'
    | 'sage'
    | 'slate'
    | 'sand'
    | 'gray'
    | 'mauve'
    | 'brown'
    | 'bronze'
    | 'gold'
    | 'red'
    | 'crimson'
    | 'tomato'
    | 'pink'
    | 'plum'
    | 'indigo'
    | 'iris'
    | 'violet'
    | 'purple'
    | 'amber'
    | 'blue'
    | 'cyan'
    | 'grass'
    | 'green'
    | 'jade'
    | 'lime'
    | 'mint'
    | 'orange'
    | 'sky'
    | 'teal'
    | 'yellow'
    | null;
  /**
   * Height: `sm` 32 px (chips, toolbars; hit area extended to 44) or `md`
   * 40 px (segments, standalone; default, chips default to `sm`).
   */
  size?: 'sm' | 'md' | null;
}
ToggleVariant

A Toggle’s look (§9.4).

type ToggleVariant = 'outline' | 'segment' | 'chip'<'outline' | 'segment' | 'chip' | null | undefined>

Specification: DESIGN-SYSTEM.md §9.4 (toggle and toggle group, filter chips and segments).