FairGarden Design

Switch

A Base UI Switch for a setting that takes effect the moment it flips, with a label and an "On" / "Off" state word.

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

Label the setting, not the action. In a form that is submitted later, use a Checkbox instead, and never use a switch for a choice that isn't on or off. Off → on changes the thumb's position and shape (ring → disc), the edge weight and the fill, so the state never rests on fill alone.

States

States

kind="row" (default) puts the label left and the switch right, with a hairline rule under each row; kind="inline" puts the switch first. Hover gives an off track a soft --primary3 fill where soft fills apply, lightens an on track to --role-select-hover and underlines the label; the edge and thumb never change. Disabled draws a dotted track and a muted thumb ring. While a change saves remotely, pass aria-busy: the state word reads "Saving…" and the switch is inert; on failure revert checked and explain in a toast.

SwitchStates.tsx
'use client'

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

/**
 * A settings list of rows (on, off, disabled, and a remote save that shows
 * "Saving…"), then an inline switch.
 */
export function SwitchStates() {
  const [alerts, setAlerts] = React.useState(true)
  const [saving, setSaving] = React.useState(false)

  const save = (next: boolean) => {
    setSaving(true)
    window.setTimeout(() => {
      setAlerts(next)
      setSaving(false)
    }, 1200)
  }

  return (
    <div className={styles.stack}>
      <div>
        <Switch
          checked={alerts}
          onCheckedChange={save}
          aria-busy={saving}
          description="Applies at once; saving takes a moment."
        >
          Email Alerts
        </Switch>
        <Switch>Trail Closures</Switch>
        <Switch disabled>Text Messages</Switch>
        <Switch disabled defaultChecked>
          Member Mail
        </Switch>
      </div>
      <Switch kind="inline" defaultChecked>
        Show Elevation
      </Switch>
    </div>
  )
}

Primary and secondary

Primary and secondary

secondary drives the on track and thumb through --role-select; primary drives the off track, the thumb ring, the label and the state word. Neither is defaulted.

SwitchColor.tsx
import { Switch } from '@fairgarden-private/design/components/Switch'
import styles from './color.module.css'

/**
 * `secondary` drives the on track and thumb through --role-select;
 * `primary` drives the off track, thumb ring, label and state word.
 */
export function SwitchColor() {
  return (
    <div className={styles.grid}>
      <Switch kind="inline" defaultChecked>
        Scope Colors
      </Switch>
      <Switch kind="inline" defaultChecked secondary="indigo">
        Secondary Indigo
      </Switch>
      <Switch kind="inline" primary="plum">
        Primary Plum
      </Switch>
    </div>
  )
}

On grounds

On paper and forest

On is green on paper and the inverse pair on forest and the saturated grounds. In print the graphic is hidden and each row prints as "Label: On".

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

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

/** On is green on paper and the inverse pair on forest; position, thumb shape and edge weight carry it on both. */
export function SwitchGrounds() {
  return (
    <div className={styles.row}>
      {presets.map((preset) => (
        <PresetGround key={preset} preset={preset} className={styles.face}>
          <span className={styles.name}>{preset}</span>
          <div>
            <Switch defaultChecked>Map Labels</Switch>
            <Switch>Contour Lines</Switch>
            <Switch disabled>Satellite View</Switch>
          </div>
        </PresetGround>
      ))}
    </div>
  )
}

API Reference

A Base UI Switch for a setting that takes effect the moment it flips. Off → on changes position, thumb shape (ring → disc), edge weight and fill, so state is never fill alone [D15]. While the change is saving, pass aria-busy: the state word reads “Saving…” and the switch is inert; on failure revert checked and explain in a toast (§10.17).

PropTypeDescription
busyLabel
string | undefined

The state word while aria-busy is set; default “Saving…".

description
React.ReactNode | undefined

Optional helper text under the label, in sentence case.

kind
'row' | 'inline' | null | undefined

row (default): label left, switch right, the row a hit target with a hairline rule. inline: switch, state word, then label.

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: the off track and thumb, label and state word. Never defaulted; omitted, it inherits the scope [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 on track’s fill and edge and the on thumb, through --role-select. Omitted, it inherits the scope.

stateLabels
SwitchStateLabels | undefined

The state words; default “On” / “Off”. Set in type-label caps.

children
React.ReactNode | undefined

The label: name the setting, not the action, in title case [D160].

className
string | undefined

Class for the row (the base part).

SwitchProps

Props for Switch: Base UI Switch.Root props (on the track) plus the kind, color axes and the row’s content. className goes on the row.

type SwitchProps = {
  /**
   * `row` (default): label left, switch right, the row a hit target with a
   * hairline rule. `inline`: switch, state word, then label.
   */
  kind?: 'row' | 'inline' | null;
  /**
   * Primary Radix scale: the off track and thumb, label and state word.
   * Never defaulted; omitted, it inherits the scope [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: the on track's fill and edge and the on thumb,
   * through --role-select. Omitted, it inherits the scope.
   */
  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;
  /** Class for the row (the `base` part). */
  className?: string;
  /** The label: name the setting, not the action, in title case [D160]. */
  children?: React.ReactNode;
  /** Optional helper text under the label, in sentence case. */
  description?: React.ReactNode;
  /** The state words; default "On" / "Off". Set in `type-label` caps. */
  stateLabels?: SwitchStateLabels;
  /** The state word while `aria-busy` is set; default "Saving…". */
  busyLabel?: string;
}
switchRoot
SwitchStateLabels

The state words shown beside the track.

type SwitchStateLabels = {
  /** Shown while on. Default "On". */
  on: string;
  /** Shown while off. Default "Off". */
  off: string;
}

Specification: DESIGN-SYSTEM.md §10.9 (switch) and §10.1 (the selected fill).