FairGarden Design

Number Field

Quantities, amounts and measurements where stepping helps: Base UI's NumberField, inside a Field. Codes that only look like numbers, such as postcodes and phone numbers, are Input; approximate values across a range use Slider.

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

The value uses tabular figures, so it never jitters while stepping. Show units on screen and in print: suffix follows the value with a no-break space, in the muted ink. At the minimum or maximum the matching step cell is disabled; out-of-range errors read "Enter 1–12", with an en dash.

Kinds and states

Kinds and states

stepper (default) joins square − and + cells to the input, divided by rules, with one --radius-1 outer radius; holding a cell takes the inverse pair. amount drops the steppers and sets the value in the display serif. readout puts circular − / + buttons around a large mono value, for dashboards. label with scrub makes the label a scrub area for fine pointers. Steppers suit ranges of up to about 20 steps.

 adults
$  USD
Water
litres
Enter 1–12.
NumberFieldStates.tsx
import { Field, FieldError, FieldLabel } from '@fairgarden-private/design/components/Field'
import { NumberField } from '@fairgarden-private/design/components/NumberField'
import styles from './states.module.css'

/**
 * The stepper (at its minimum, so − is disabled), the amount and the
 * readout, then invalid, read-only and disabled fields.
 */
export function NumberFieldStates() {
  return (
    <div className={styles.stack}>
      <Field>
        <FieldLabel>Tickets</FieldLabel>
        <NumberField min={1} max={12} defaultValue={1} suffix="adults" />
      </Field>
      <Field>
        <FieldLabel>Donation</FieldLabel>
        <NumberField kind="amount" prefix="$" suffix="USD" defaultValue={50} min={1} />
      </Field>
      <Field>
        <NumberField label="Water" scrub kind="readout" defaultValue={2.5} step={0.5} suffix="litres" />
      </Field>
      <Field invalid>
        <FieldLabel>Group Size</FieldLabel>
        <NumberField min={1} max={12} defaultValue={14} allowOutOfRange />
        <FieldError match>Enter 1–12.</FieldError>
      </Field>
      <Field>
        <FieldLabel>Nights Booked</FieldLabel>
        <NumberField readOnly defaultValue={3} />
      </Field>
      <Field disabled>
        <FieldLabel>Guides</FieldLabel>
        <NumberField defaultValue={2} />
      </Field>
    </div>
  )
}

Primary and secondary

Primary and secondary

primary sets the group edge, dividers, glyphs and value. secondary is unused at rest and becomes the danger scale while the Field is invalid.

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

/** `primary` recolors the group edge, dividers, glyphs, value and the held cell's inverse pair. */
export function NumberFieldColor() {
  return (
    <div className={styles.stack}>
      <Field>
        <FieldLabel>Primary Plum</FieldLabel>
        <NumberField primary="plum" defaultValue={4} min={0} max={20} suffix="kg" />
      </Field>
      <Field>
        <FieldLabel>Primary Slate</FieldLabel>
        <NumberField primary="slate" kind="amount" prefix="€" defaultValue={25} />
      </Field>
    </div>
  )
}

On grounds

On paper and forest

On forest the same roles resolve to the dark scale: light glyphs, the deep rule, and a light held cell.

paper

 km

forest

 km
NumberFieldGrounds.tsx
import { Field, FieldLabel } from '@fairgarden-private/design/components/Field'
import { PresetGround } from '@/components/PresetGround'
import { NumberField } from '@fairgarden-private/design/components/NumberField'
import styles from './grounds.module.css'

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

/** The stepper on paper and forest: the held cell is a light fill with a dark glyph on the deep ground. */
export function NumberFieldGrounds() {
  return (
    <div className={styles.row}>
      {presets.map((preset) => (
        <PresetGround key={preset} preset={preset} className={styles.face}>
          <p className={styles.name}>{preset}</p>
          <Field>
            <FieldLabel>Campers</FieldLabel>
            <NumberField min={1} max={8} defaultValue={2} />
          </Field>
          <Field>
            <FieldLabel>Distance</FieldLabel>
            <NumberField kind="amount" defaultValue={12} suffix="km" />
          </Field>
        </PresetGround>
      ))}
    </div>
  )
}

API Reference

A Base UI NumberField inside a Field. At the minimum or maximum the matching cell is disabled; out-of-range errors read “Enter 1–12″ with an en dash [D32]. Use steppers for up to about 20 steps and tabular figures throughout.

PropTypeDescription
label
React.ReactNode | undefined

A label rendered inside the root (a FieldLabel). Use it with scrub; otherwise a FieldLabel beside the NumberField in its Field works.

decrementLabel
string | undefined

The decrement cell’s accessible name. Default “Decrease” (Base UI).

incrementLabel
string | undefined

The increment cell’s accessible name. Default “Increase” (Base UI).

kind
'stepper' | 'amount' | 'readout' | undefined

stepper (default): square −/+ cells joined to the input. amount: no steppers, the value in the display serif. readout: circular −/+ buttons around a large mono value, for dashboards.

placeholder
string | undefined

The input’s placeholder, ending in “…".

plate
boolean | false | undefined

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

prefix
React.ReactNode | undefined
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: group edge, dividers, glyphs and value. Never defaulted; omitted, it inherits the scope [D133].

scrub
boolean | undefined

Makes label a scrub area: drag it to change the value (fine pointers).

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; the danger scale while invalid.

suffix
React.ReactNode | undefined

The unit, in --role-muted after the value with a no-break space (“kg”); on the readout, the caption under the value. Show units on screen and in print.

className
string | undefined

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

numberField
NumberFieldProps

Props for NumberField: Base UI NumberField.Root props plus the kind, units and color axes.

type NumberFieldProps = (
  { kind?: 'stepper' | 'amount'; plate?: boolean } | { kind: 'readout'; plate?: false }
) & {
  /** Class names for the root, added after the module's own. */
  className?: string;
  /** A prefix unit, in --role-muted ("$"). */
  prefix?: React.ReactNode;
  /**
   * The unit, in --role-muted after the value with a no-break space
   * ("kg"); on the readout, the caption under the value. Show units on
   * screen and in print.
   */
  suffix?: React.ReactNode;
  /**
   * A label rendered inside the root (a `FieldLabel`). Use it with `scrub`;
   * otherwise a `FieldLabel` beside the NumberField in its `Field` works.
   */
  label?: React.ReactNode;
  /** Makes `label` a scrub area: drag it to change the value (fine pointers). */
  scrub?: boolean;
  /** The input's placeholder, ending in "…". */
  placeholder?: string;
  /** The decrement cell's accessible name. Default "Decrease" (Base UI). */
  decrementLabel?: string;
  /** The increment cell's accessible name. Default "Increase" (Base UI). */
  incrementLabel?: string;
  /**
   * Primary Radix scale: group edge, dividers, glyphs and value. 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; the danger scale while invalid. */
  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;
}

Specification: DESIGN-SYSTEM.md §10.4 (number field), with the stepper confirmed in D174.