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.
Up to 200 characters.
'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.
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
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.
| Prop | Type | Description |
|---|---|---|
label | | The label drawn inside the box, title case [D160]. Required with |
action | | The butted action cell; one-field forms only. |
butted | | Joins the action cell at the box’s end: --ds-radius-none at the seam. |
icon | | A leading icon: Material Symbols Rounded, inline tier (§6.10). |
labelInside | | The |
limit | | |
multiline | |
|
plate | | The box face becomes a nested |
prefix | | A prefix unit inside the box, in --role-muted ("$"). |
primary | | Primary Radix scale: box edge, value, placeholder, icon and focus ring. Never defaulted; omitted, it inherits the scope [D133]. |
reveal | | Password fields: adds a “Show” / “Hide” text toggle, never an eye icon alone (§10.3). |
revealLabels | | The toggle’s two labels. Default |
rows | | |
secondary | | Secondary Radix scale. Unused at rest; while the Field is invalid it is the danger scale (the error edge). |
suffix | | A suffix unit inside the box, in --role-muted (“USD”). Never the unit only in the placeholder. |
variant | |
|
className | | Class names for the box (the root), added after the module’s own. |
style | | Inline style for the box: set its width to the expected answer (postcode |
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).