FairGarden Design

Select

One choice from a known list of about 5–15 options: Base UI's Select, inside a Field. For 2–4 options that should stay visible use Radio or Toggle Group; for long or searchable lists use Combobox.

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

Pass items (options, or titled groups) and a placeholder that ends in "…". Label the trigger with <FieldLabel nativeLabel={false}>, which focuses it without opening the popup. The popup opens --size-px-2 below the trigger, never on hover and never as a sheet; at most 8 items show before it scrolls. For custom rows, pass SelectItem, SelectGroup and SelectSeparator as children.

Variants and states

Variants and states

The boxed trigger is the field box; variant="underline" sets the trigger over a --border-size-2 --role-accent underline that hugs its content. Highlighted items take the --primary4 soft fill and a --ds-stroke-3 start bar; the chosen item a leading ✓ and --font-weight-6; a disabled item stays muted and is never highlighted. An option with a swatch draws the product's color before its name.

Trail
Sort By
Region
Jacket Color
Start Point
Choose where your walk starts.
Guide
SelectStates.tsx
import { Field, FieldError, FieldLabel } from '@fairgarden-private/design/components/Field'
import { Select } from '@fairgarden-private/design/components/Select'
import styles from './states.module.css'

const trails = [
  { value: 'ridge', label: 'Ridge Loop' },
  { value: 'marsh', label: 'Marsh Boardwalk' },
  { value: 'falls', label: 'Falls Trail' },
  { value: 'summit', label: 'Summit Path', disabled: true },
  { value: 'meadow', label: 'Meadow Walk' },
]

const regions = [
  {
    label: 'North',
    items: [
      { value: 'pines', label: 'Pine Barrens' },
      { value: 'lakes', label: 'Lake Country' },
    ],
  },
  {
    label: 'South',
    items: [
      { value: 'delta', label: 'River Delta' },
      { value: 'dunes', label: 'Coastal Dunes' },
    ],
  },
]

const colors = [
  { value: 'moss', label: 'Moss Green', swatch: '#5b7f3a' },
  { value: 'clay', label: 'Clay Orange', swatch: '#c2622d' },
  { value: 'slate', label: 'Slate Blue', swatch: '#4f6a86' },
]

/**
 * Boxed and underline triggers, groups, a swatch-led select, then invalid
 * and disabled triggers. The swatch colors are content, like a photograph.
 */
export function SelectStates() {
  return (
    <div className={styles.stack}>
      <Field>
        <FieldLabel nativeLabel={false}>Trail</FieldLabel>
        <Select items={trails} placeholder="Choose a trail…" />
      </Field>
      <Field>
        <FieldLabel nativeLabel={false}>Sort By</FieldLabel>
        <Select variant="underline" items={trails} defaultValue="marsh" />
      </Field>
      <Field>
        <FieldLabel nativeLabel={false}>Region</FieldLabel>
        <Select items={regions} placeholder="Choose a region…" />
      </Field>
      <Field>
        <FieldLabel nativeLabel={false}>Jacket Color</FieldLabel>
        <Select items={colors} defaultValue="moss" />
      </Field>
      <Field invalid>
        <FieldLabel nativeLabel={false}>Start Point</FieldLabel>
        <Select items={trails} placeholder="Choose a start point…" />
        <FieldError match>Choose where your walk starts.</FieldError>
      </Field>
      <Field disabled>
        <FieldLabel nativeLabel={false}>Guide</FieldLabel>
        <Select items={trails} defaultValue="falls" />
      </Field>
    </div>
  )
}

Primary and secondary

Primary and secondary

primary sets the trigger's edge, value and chevron; secondary the underline. While invalid, the danger scale turns the boxed edge and the underline to the error ink. The popup never inherits the trigger's scales.

Primary Plum
Secondary Orange
SelectColor.tsx
import { Field, FieldLabel } from '@fairgarden-private/design/components/Field'
import { Select } from '@fairgarden-private/design/components/Select'
import styles from './color.module.css'

const sizes = [
  { value: 's', label: 'Small' },
  { value: 'm', label: 'Medium' },
  { value: 'l', label: 'Large' },
]

/**
 * `primary` recolors the trigger; `secondary` drives the underline's
 * accent. The popup is always the white scope with its own defaults.
 */
export function SelectColor() {
  return (
    <div className={styles.stack}>
      <Field>
        <FieldLabel nativeLabel={false}>Primary Plum</FieldLabel>
        <Select primary="plum" items={sizes} defaultValue="m" />
      </Field>
      <Field>
        <FieldLabel nativeLabel={false}>Secondary Orange</FieldLabel>
        <Select variant="underline" secondary="orange" items={sizes} defaultValue="l" />
      </Field>
    </div>
  )
}

On grounds

On paper and forest

The trigger follows its ground; the popup renders in a portal as the white scope, framed by --border-size-2 --primary12 in both modes.

paper

Season
Show

forest

Season
Show
SelectGrounds.tsx
import { Field, FieldLabel } from '@fairgarden-private/design/components/Field'
import { PresetGround } from '@/components/PresetGround'
import { Select } from '@fairgarden-private/design/components/Select'
import styles from './grounds.module.css'

const seasons = [
  { value: 'spring', label: 'Spring' },
  { value: 'summer', label: 'Summer' },
  { value: 'autumn', label: 'Autumn' },
  { value: 'winter', label: 'Winter' },
]

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

/**
 * The trigger follows its ground (on forest the underline takes the deep
 * accent); the popup is always the white scope, which follows the page mode.
 */
export function SelectGrounds() {
  return (
    <div className={styles.row}>
      {presets.map((preset) => (
        <PresetGround key={preset} preset={preset} className={styles.face}>
          <p className={styles.name}>{preset}</p>
          <Field>
            <FieldLabel nativeLabel={false}>Season</FieldLabel>
            <Select items={seasons} defaultValue="autumn" />
          </Field>
          <Field>
            <FieldLabel nativeLabel={false}>Show</FieldLabel>
            <Select variant="underline" items={seasons} defaultValue="spring" />
          </Field>
        </PresetGround>
      ))}
    </div>
  )
}

API Reference

Select

A Base UI Select inside a Field (label it with <FieldLabel nativeLabel={false}>). The popup opens below the trigger, never on hover, and is never a sheet; use Combobox for long or searchable lists and Radio for 2–4 visible options.

PropTypeDescription
aria-label
string | undefined

Names the trigger when no visible label exists. Prefer a FieldLabel.

items
SelectItems<Value> | undefined

The options (or titled groups). Rendered as items unless children are given; always used to show the chosen option’s label.

placeholder
string | undefined

Shown in --role-muted while nothing is chosen; end it with “…".

plate
boolean | false | undefined

The trigger’s face becomes a nested white scope; patterned grounds only (§10.1).

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

Primary Radix scale: trigger edge, value, chevron and focus ring. Never defaulted; omitted, it inherits the scope [D133].

secondary
| 'olive'
| 'sage'
| 'slate'
| 'sand'
| 'gray'
| 'mauve'
| 'brown'
| 'bronze'
| 'gold'
| 'red'
| 'ruby'
| '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 underline variant’s underline; the danger scale while invalid.

variant
'outline' | 'underline' | undefined

outline (default) is the boxed field; underline is the trigger over a --role-accent underline that hugs its content.

children
React.ReactNode | undefined

Custom list content (SelectItem, SelectGroup, SelectSeparator) instead of items.

className
string | undefined

Class names for the trigger (the root part), added after the module’s own.

SelectItem, SelectGroup and SelectSeparator

One option: a --ds-size-hit row. Highlighted rows take the --primary4 soft fill and the --ds-stroke-3 start bar; the chosen row a leading ✓ and --font-weight-6 [D145].

PropTypeDescription
swatch
string | undefined

The product’s color (content) as a ringed disc before the label.

className
string | undefined

Extra class names, added after the module’s own.

A titled group of items.

PropTypeDescription
label
React.ReactNode | undefined

The group label, in type-label caps and --role-muted.

className
string | undefined

The one popup group separator: --border-size-1 in --role-rule.

PropTypeDescription
className
string | undefined

Additional types

select
type select = select
SelectGroupProps

Props for SelectGroup: Base UI Select.Group props plus its label.

type SelectGroupProps = {
  /** The group label, in `type-label` caps and --role-muted. */
  label: React.ReactNode;
  className?: string;
}
SelectItemProps

Props for SelectItem: Base UI Select.Item props plus an optional swatch.

type SelectItemProps = {
  /** Extra class names, added after the module's own. */
  className?: string;
  /** The product's color (content) as a ringed disc before the label. */
  swatch?: string;
}
SelectOption

One option: its value, its label (the words the trigger shows) and an optional swatch.

type SelectOption<Value = string> = {
  value: Value;
  /** The option's words; with a swatch they name the color. */
  label: string;
  /**
   * The product's color, supplied as data like a photograph (a CSS color
   * string); drawn as a ringed disc before the label. Always name the color
   * in `label`.
   */
  swatch?: string;
  disabled?: boolean;
}
SelectOptionGroup

A titled group of options; groups are divided by the popup’s separator.

type SelectOptionGroup<Value = string> = {
  /** The group label, set in `type-label` caps. */
  label: string;
  items: SelectOption<Value>[];
}
SelectProps

Props for Select: Base UI Select.Root props plus the options, variant and color axes.

type SelectProps<Value = string> = (
  { variant?: 'outline'; plate?: boolean } | { variant: 'underline'; plate?: false }
) & {
  /**
   * The options (or titled groups). Rendered as items unless `children`
   * are given; always used to show the chosen option's label.
   */
  items?: SelectItems<Value>;
  /** Custom list content (`SelectItem`, `SelectGroup`, `SelectSeparator`) instead of `items`. */
  children?: React.ReactNode;
  /** Shown in --role-muted while nothing is chosen; end it with "…". */
  placeholder?: string;
  /** Class names for the trigger (the root part), added after the module's own. */
  className?: string;
  /** Names the trigger when no visible label exists. Prefer a `FieldLabel`. */
  'aria-label'?: string;
  /**
   * Primary Radix scale: trigger edge, value, chevron and focus ring. Never
   * defaulted; omitted, it inherits the scope [D133].
   */
  primary?:
    | 'olive'
    | 'sage'
    | 'slate'
    | 'sand'
    | 'gray'
    | 'mauve'
    | 'brown'
    | 'bronze'
    | 'gold'
    | 'red'
    | 'ruby'
    | 'crimson'
    | 'tomato'
    | 'pink'
    | 'plum'
    | 'indigo'
    | 'iris'
    | 'violet'
    | 'purple'
    | null;
  /**
   * Secondary Radix scale: the underline variant's underline; the danger
   * scale while invalid.
   */
  secondary?:
    | 'olive'
    | 'sage'
    | 'slate'
    | 'sand'
    | 'gray'
    | 'mauve'
    | 'brown'
    | 'bronze'
    | 'gold'
    | 'red'
    | 'ruby'
    | 'crimson'
    | 'tomato'
    | 'pink'
    | 'plum'
    | 'indigo'
    | 'iris'
    | 'violet'
    | 'purple'
    | 'amber'
    | 'blue'
    | 'cyan'
    | 'grass'
    | 'green'
    | 'jade'
    | 'lime'
    | 'mint'
    | 'orange'
    | 'sky'
    | 'teal'
    | 'yellow'
    | null;
}
SelectSeparatorProps

Props for SelectSeparator: Base UI Select.Separator props.

type SelectSeparatorProps = { className?: string }

Specification: DESIGN-SYSTEM.md §10.5 (select) and §10.1 (the overlay surface).