FairGarden Design

Field

Wires every control's label, description and error: a Base UI Field.Root whose label, control, description and error stack in that order.

import {
  Field,
  FieldDescription,
  FieldError,
  FieldLabel,
} from '@fairgarden-private/design/components/Field'

Labels are authored in title case up to about four words and sentence case beyond; helper text is sentence case. Put "(optional)" in the label with optional, never an asterisk alone. While Base UI reports the field invalid, the Field passes the danger scale to its error and, through context, to its text-entry or select control; value controls keep their own secondary. For a Select or Combobox, label the trigger with <FieldLabel nativeLabel={false}>.

Base UI validates on submit and then re-validates a failed field as it changes. To show a server error, pass invalid to the Field and match to its FieldError.

States

States

A rest field with a description, an optional field, an invalid field (the danger ◆ plus a message that says how to fix it) and a disabled field, whose label and value take the muted ink and whose edge turns dotted.

We send one trail report a month.

Postcode is too short. Enter all 5 digits.

Assigned when your membership starts.

FieldStates.tsx
import {
  Field,
  FieldDescription,
  FieldError,
  FieldLabel,
} from '@fairgarden-private/design/components/Field'
import { Input } from '@fairgarden-private/design/components/Input'
import styles from './states.module.css'

/**
 * Rest, optional, invalid and disabled fields. The invalid field passes the
 * danger scale to its control and error; the label stays --primary12.
 */
export function FieldStates() {
  return (
    <div className={styles.stack}>
      <Field>
        <FieldLabel>Email Address</FieldLabel>
        <Input type="email" placeholder="Email address…" />
        <FieldDescription>We send one trail report a month.</FieldDescription>
      </Field>
      <Field>
        <FieldLabel optional>Phone Number</FieldLabel>
        <Input type="tel" />
      </Field>
      <Field invalid>
        <FieldLabel>Postcode</FieldLabel>
        <Input defaultValue="12" style={{ inlineSize: '8ch' }} />
        <FieldError match>Postcode is too short. Enter all 5 digits.</FieldError>
      </Field>
      <Field disabled>
        <FieldLabel>Member Number</FieldLabel>
        <Input defaultValue="FG-2041" />
        <FieldDescription>Assigned when your membership starts.</FieldDescription>
      </Field>
    </div>
  )
}

Primary and secondary

Primary and secondary

primary sets the label and, by inheritance, the control's edge, value and ring. secondary is unused at rest: while the field is invalid the danger scale replaces it.

Primary plum: label, edge, value and ring.

Groups are 12 or fewer. Enter 1–12.
FieldColor.tsx
import {
  Field,
  FieldDescription,
  FieldError,
  FieldLabel,
} from '@fairgarden-private/design/components/Field'
import { Input } from '@fairgarden-private/design/components/Input'
import styles from './color.module.css'

/**
 * `primary` recolors the label, description and, by inheritance, the
 * control. While invalid the error always takes the danger scale.
 */
export function FieldColor() {
  return (
    <div className={styles.stack}>
      <Field primary="plum">
        <FieldLabel>Trail Name</FieldLabel>
        <Input placeholder="Ridge loop…" />
        <FieldDescription>Primary plum: label, edge, value and ring.</FieldDescription>
      </Field>
      <Field primary="slate" secondary="indigo" invalid>
        <FieldLabel>Group Size</FieldLabel>
        <Input defaultValue="40" />
        <FieldError match>Groups are 12 or fewer. Enter 1–12.</FieldError>
      </Field>
    </div>
  )
}

On grounds

On paper and forest

Fields stay open on every ground: the rule, muted and status roles resolve per ground, so the same markup reads on paper and on forest.

paper

As it appears on your card.

Enter an email address, like name@example.com.

forest

As it appears on your card.

Enter an email address, like name@example.com.
FieldGrounds.tsx
import {
  Field,
  FieldDescription,
  FieldError,
  FieldLabel,
} from '@fairgarden-private/design/components/Field'
import { PresetGround } from '@/components/PresetGround'
import { Input } from '@fairgarden-private/design/components/Input'
import styles from './grounds.module.css'

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

/** The same fields on paper and forest: the roles resolve per ground. */
export function FieldGrounds() {
  return (
    <div className={styles.row}>
      {presets.map((preset) => (
        <PresetGround key={preset} preset={preset} className={styles.face}>
          <p className={styles.name}>{preset}</p>
          <Field>
            <FieldLabel>Your Name</FieldLabel>
            <Input placeholder="First and last name…" />
            <FieldDescription>As it appears on your card.</FieldDescription>
          </Field>
          <Field invalid>
            <FieldLabel>Email Address</FieldLabel>
            <Input defaultValue="heron@" />
            <FieldError match>Enter an email address, like name@example.com.</FieldError>
          </Field>
        </PresetGround>
      ))}
    </div>
  )
}

API Reference

Field

A Base UI Field.Root: the stack of label, control, description and error. Validation follows §10.2: validate on submit, then on blur for a field that has already failed (validationMode on the Form).

PropTypeDescription
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: the label, description and, by inheritance, the control’s edge, value and focus ring. Never defaulted [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. Unused at rest; while the field is invalid the danger scale replaces it on the error and the control (§10.1).

FieldLabel

The field label: type-field-label in --primary12, authored in title case up to about four words, sentence case for longer or question labels [D160]. For a Select or Combobox trigger pass nativeLabel={false}: the label then renders a div that focuses the control without opening it.

PropTypeDescription
optional
boolean | string | undefined

Appends the marker inside the label (§10.1): true writes “(optional)"; a string writes that marker instead, such as “(required)” where most fields are optional. Never an asterisk alone.

FieldDescription and FieldError

Helper text: type-caption in --role-muted, --size-px-1 below the box; sentence case. Base UI links it to the control with aria-describedby.

The error message, shown by Base UI while the field is invalid (or for the match it names): the danger glyph in --role-status, then the message in type-caption at --font-weight-6 in --primary12. Word it as what happened plus how to fix it (§1.9.2). Without children it shows the browser’s validation message.

Additional types

dangerScale

The §1.5.4 danger scale, passed as secondary while a field is invalid [D129].

type dangerScale = 'red'
field
type field = field
FieldDescriptionProps

Props for FieldDescription: Base UI Field.Description props.

type FieldDescriptionProps = FieldDescription.FieldDescriptionProps
FieldErrorProps

Props for FieldError: Base UI Field.Error props, without render.

type FieldErrorProps = Omit<FieldError.FieldErrorProps, 'render'>
FieldLabelProps

Props for FieldLabel: Base UI Field.Label props plus the “(optional)” marker.

type FieldLabelProps = FieldLabelProps & { optional?: boolean | string }
FieldProps

Props for Field: Base UI Field.Root props plus the color axes.

type FieldProps = FieldRootProps & {
  primary?:
    | 'olive'
    | 'sage'
    | 'slate'
    | 'sand'
    | 'gray'
    | 'mauve'
    | 'brown'
    | 'bronze'
    | 'gold'
    | 'red'
    | 'ruby'
    | 'crimson'
    | 'tomato'
    | 'pink'
    | 'plum'
    | 'indigo'
    | 'iris'
    | 'violet'
    | 'purple'
    | null;
  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;
}

Specification: DESIGN-SYSTEM.md §10.1 (the field box and state grammar) and §10.2 (field, fieldset and form).