FairGarden Design

Input

Single-line text, email, password, telephone and URL, plus multi-line text: Base UI's Input (Field.Control) in the field box, inside a Field.

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

The box is open by default: a --border-size-1 --role-rule edge that steps to --primary12 on hover, at the same weight. The ring shows on any focus, pointer included. The placeholder is never the label and ends in "…". className and style go to the box, so set its width to the expected answer (style={{ inlineSize: '8ch' }} for a postcode). Stepped numbers use NumberField, choices use Select or Combobox, and site search is Search.

Variants and states

Variants and states

prefix, suffix and icon adorn the box. labelInside draws the label in the 56 px box, with the value below it, larger. variant="underline" is a lone bottom rule for dense settings rows and printable blanks. multiline is the textarea, with an optional limit count. reveal adds a "Show" / "Hide" toggle to a password. butted="end" joins the action cell: the icon-only 64 × 48 px cell at base, with its label from --md-n-above when labelled. Invalid fields take a --border-size-2 danger edge; read-only fields lose the box and keep a bottom rule; disabled fields draw a dotted edge.

$USD
0/200

Up to 200 characters.

Enter a full address, starting with https://.
InputStates.tsx
'use client'

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'

/**
 * The boxed field and its builds: adorned, label-inside, underline,
 * textarea with a count, password reveal and the butted action, then the
 * invalid, read-only and disabled states.
 */
export function InputStates() {
  return (
    <div className={styles.stack}>
      <Field>
        <FieldLabel>Email Address</FieldLabel>
        <Input type="email" placeholder="Email address…" />
      </Field>
      <Field>
        <FieldLabel>Donation</FieldLabel>
        <Input inputMode="decimal" prefix="$" suffix="USD" defaultValue="50" />
      </Field>
      <Field>
        <FieldLabel>Find a Trail</FieldLabel>
        <Input icon="search" placeholder="Trail or town…" />
      </Field>
      <Field>
        <Input labelInside label="Full Name" defaultValue="Ada Heron" />
      </Field>
      <Field>
        <FieldLabel>Display Name</FieldLabel>
        <Input variant="underline" defaultValue="heron_watch" />
      </Field>
      <Field>
        <FieldLabel>Trip Notes</FieldLabel>
        <Input multiline limit={200} placeholder="What did you see…" />
        <FieldDescription>Up to 200 characters.</FieldDescription>
      </Field>
      <Field>
        <FieldLabel>Password</FieldLabel>
        <Input type="password" reveal defaultValue="marsh-lantern" />
      </Field>
      <Field>
        <FieldLabel>Newsletter</FieldLabel>
        <Input
          type="email"
          placeholder="Email address…"
          butted="end"
          action={{ label: 'Sign Up', labelled: true, type: 'button' }}
        />
      </Field>
      <Field invalid>
        <FieldLabel>Website</FieldLabel>
        <Input type="url" defaultValue="fairgarden" />
        <FieldError match>Enter a full address, starting with https://.</FieldError>
      </Field>
      <Field>
        <FieldLabel>Member Since</FieldLabel>
        <Input readOnly defaultValue="March 2019" />
      </Field>
      <Field disabled>
        <FieldLabel>Referral Code</FieldLabel>
        <Input placeholder="Code…" />
      </Field>
    </div>
  )
}

Primary and secondary

Primary and secondary

primary sets the edge, value, icon and ring. secondary is unused at rest; while the Field is invalid it becomes the danger scale. The action cell takes the scope's action scale.

InputColor.tsx
import { Field, FieldLabel } from '@fairgarden-private/design/components/Field'
import { Input } from '@fairgarden-private/design/components/Input'
import styles from './color.module.css'

/**
 * `primary` recolors the edge, value, icon and ring; the butted action
 * keeps the scope's action scale, as a `solid` Button does.
 */
export function InputColor() {
  return (
    <div className={styles.stack}>
      <Field>
        <FieldLabel>Primary Plum</FieldLabel>
        <Input primary="plum" icon="search" placeholder="Search the guide…" />
      </Field>
      <Field>
        <FieldLabel>Primary Indigo</FieldLabel>
        <Input
          primary="indigo"
          butted="end"
          action={{ label: 'Send', type: 'button' }}
          placeholder="Email address…"
        />
      </Field>
    </div>
  )
}

On grounds

On paper and forest

The same field on paper and forest. plate makes the box a nested white face: use it only on a patterned ground (grain, speckle, dotgrid), where a --role-rule edge may not sit.

paper

forest

InputGrounds.tsx
import { Field, 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

/**
 * Open fields on paper and forest; the action cell stays amber, its edge
 * equal to its fill on the deep ground. The plate is for patterned grounds.
 */
export function InputGrounds() {
  return (
    <div className={styles.row}>
      {presets.map((preset) => (
        <PresetGround key={preset} preset={preset} className={styles.face}>
          <p className={styles.name}>{preset}</p>
          <Field>
            <FieldLabel>Email Address</FieldLabel>
            <Input
              type="email"
              placeholder="Email address…"
              butted="end"
              action={{ label: 'Sign Up', type: 'button' }}
            />
          </Field>
          <Field>
            <FieldLabel>On a Plate</FieldLabel>
            <Input plate placeholder="For patterned grounds…" />
          </Field>
        </PresetGround>
      ))}
    </div>
  )
}

API Reference

A text field on Base UI’s Input (Field.Control), inside a Field. The placeholder is never the label; end it with “…". A plate belongs only on a patterned ground; everywhere else the box is open.

PropTypeDescription
label
React.ReactNode | undefined

The label drawn inside the box, title case [D160]. Required with labelInside.

action
InputAction | undefined

The butted action cell; one-field forms only.

butted
'end' | undefined

Joins the action cell at the box’s end: --ds-radius-none at the seam.

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

A leading icon: Material Symbols Rounded, inline tier (§6.10).

labelInside
boolean | false | undefined

The --ds-size-control-xl box with the label inside (Audubon). Default false.

limit
number | undefined
multiline
boolean | undefined

true renders the textarea. Default false.

plate
boolean | false | undefined

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

prefix
React.ReactNode | undefined

A prefix unit inside the box, in --role-muted ("$").

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: box edge, value, placeholder, icon and focus ring. Never defaulted; omitted, it inherits the scope [D133].

reveal
boolean | undefined

Password fields: adds a “Show” / “Hide” text toggle, never an eye icon alone (§10.3).

revealLabels
[string, string] | undefined

The toggle’s two labels. Default ['Show', 'Hide'].

rows
number | undefined
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. Unused at rest; while the Field is invalid it is the danger scale (the error edge).

suffix
React.ReactNode | undefined

A suffix unit inside the box, in --role-muted (“USD”). Never the unit only in the placeholder.

variant
'outline' | 'underline' | undefined

outline (default) is the §10.1 field box; underline a lone bottom rule.

className
string | undefined

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

style
React.CSSProperties | undefined

Inline style for the box: set its width to the expected answer (postcode 8ch).

input
type input = input
InputAction

The butted action cell (§10.3): a solid Button joined to the box’s end.

type InputAction = {
  /**
   * The action's accessible name, and its visible `type-button` label from
   * `--md-n-above` when `labelled`. Title case, no tracking [D160].
   */
  label: string;
  /**
   * Shows `label` in the cell from `--md-n-above` (with `--size-px-5`
   * padding). It stays the icon-only 64 × 48 px cell at base, below
   * `--xs-n-below` and below 360 px of Form width. Default `false`.
   */
  labelled?: boolean;
  /** Busy: the cell is inert and reads `busyLabel`; an icon-only cell shows "…" [D84]. */
  busy?: boolean;
  /** The "-ing…" wording while busy, e.g. "Sending…". Default "Sending…". */
  busyLabel?: string;
  /** Default `submit`. */
  type?: 'submit' | 'button';
  onClick?: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
  disabled?: boolean;
}
inputParts

The module’s part classes, for composed components that build a field box around another Base UI control (Search’s Autocomplete input) and leave the box’s parts to this module (§1.11.1).

type inputParts = {
  control: string;
  icon: string;
  affix: string;
  actionSlot: string;
  actionIcon: string;
  actionText: string;
  edge: string;
  edgeLine: string;
}
InputProps

Props for Input: Base UI Input props (for the control) plus the variant, slot and color props. className and style go to the box.

type InputProps = (
  | {
      variant?: 'outline';
      plate?: boolean;
      multiline?: false;
      rows?: undefined;
      limit?: undefined;
      reveal?: boolean;
      revealLabels?: [string, string];
      labelInside: true;
      label: React.ReactNode;
      butted: 'end';
      action: InputAction;
    }
  | {
      variant?: 'outline';
      plate?: boolean;
      multiline?: false;
      rows?: undefined;
      limit?: undefined;
      reveal?: boolean;
      revealLabels?: [string, string];
      labelInside: true;
      label: React.ReactNode;
      butted?: undefined;
      action?: undefined;
    }
  | {
      variant?: 'outline';
      plate?: boolean;
      multiline?: false;
      rows?: undefined;
      limit?: undefined;
      reveal?: boolean;
      revealLabels?: [string, string];
      labelInside?: false;
      label?: undefined;
      butted: 'end';
      action: InputAction;
    }
  | {
      variant?: 'outline';
      plate?: boolean;
      multiline?: false;
      rows?: undefined;
      limit?: undefined;
      reveal?: boolean;
      revealLabels?: [string, string];
      labelInside?: false;
      label?: undefined;
      butted?: undefined;
      action?: undefined;
    }
  | {
      variant?: 'outline';
      plate?: boolean;
      multiline: true;
      rows?: number;
      limit?: number;
      reveal?: undefined;
      revealLabels?: undefined;
      labelInside?: false;
      label?: undefined;
      butted?: undefined;
      action?: undefined;
    }
  | {
      variant: 'underline';
      plate?: false;
      multiline?: false;
      rows?: undefined;
      limit?: undefined;
      reveal?: boolean;
      revealLabels?: [string, string];
      labelInside?: false;
      label?: undefined;
      butted?: undefined;
      action?: undefined;
    }
  | {
      variant: 'underline';
      plate?: false;
      multiline: true;
      rows?: number;
      limit?: number;
      reveal?: undefined;
      revealLabels?: undefined;
      labelInside?: false;
      label?: undefined;
      butted?: undefined;
      action?: undefined;
    }
) & {
  /** Class names for the box (the root), added after the module's own. */
  className?: string;
  /** Inline style for the box: set its width to the expected answer (postcode `8ch`). */
  style?: React.CSSProperties;
  /**
   * Primary Radix scale: box edge, value, placeholder, icon and focus ring.
   * 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. Unused at rest; while the Field is invalid it is
   * the danger scale (the error edge).
   */
  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;
  /** A leading icon: Material Symbols Rounded, inline tier (§6.10). */
  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';
  /** A prefix unit inside the box, in --role-muted ("$"). */
  prefix?: React.ReactNode;
  /** A suffix unit inside the box, in --role-muted ("USD"). Never the unit only in the placeholder. */
  suffix?: React.ReactNode;
}

Specification: DESIGN-SYSTEM.md §10.1 (the field box) and §10.3 (input).