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.
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.
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
forest
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.
| Prop | Type | Description |
|---|---|---|
aria-label | | Names the trigger when no visible label exists. Prefer a |
items | | The options (or titled groups). Rendered as items unless |
placeholder | | Shown in --role-muted while nothing is chosen; end it with “…". |
plate | | The trigger’s face becomes a nested |
primary | | Primary Radix scale: trigger edge, value, chevron and focus ring. Never defaulted; omitted, it inherits the scope [D133]. |
secondary | | Secondary Radix scale: the underline variant’s underline; the danger scale while invalid. |
variant | |
|
children | | Custom list content ( |
className | | 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].
| Prop | Type | Description |
|---|---|---|
swatch | | The product’s color (content) as a ringed disc before the label. |
className | | Extra class names, added after the module’s own. |
A titled group of items.
| Prop | Type | Description |
|---|---|---|
label | | The group label, in |
className | |
The one popup group separator: --border-size-1 in --role-rule.
| Prop | Type | Description |
|---|---|---|
className | |
Additional types
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).