Skip to content

Field

A field is the unit a form is built from: one control, the text that names it, the text that explains it, and the message shown when it fails. Wiring those four together by hand means generating ids, threading htmlFor, maintaining aria-describedby and remembering to remove the error id when the message unmounts. Field does all of it, and it is the only place name needs to be written.

field/basic.tsx

We only use this for receipts.

Every Base UI form control works inside a field with no adapter: Input, Checkbox, Switch and Select. There is no Field.Control here on purpose — Base UI's Input is Field.Control, so <Input> is the control.

Import

import { Field } from "@forte-ui/react";

Anatomy

<Field.Root name="email">
  <Field.Label>Email</Field.Label>
  <Input type="email" required />
  <Field.Description>We only use this for receipts.</Field.Description>
  <Field.Error match="valueMissing">An email address is required.</Field.Error>
</Field.Root>

Field.Root is a flex column that stretches its children — which is why an Input inside one fills the field's width without fullWidth.

Examples

Validation

validationMode decides when, and the default (onSubmit, then re-validate on change) is almost always the one you want: validating as someone types tells them their email is invalid on the third character. onBlur is the reasonable middle ground, and the demo above uses it so it has something to show without a form to submit.

One <Field.Error match> per failure replaces the browser's own wording, which differs by browser and by locale and is rarely phrased the way your product would phrase it.

Custom rules

validate handles anything the platform's constraints cannot express. Return a string, an array of strings, or null; async is supported. With validationMode="onChange", validationDebounceTime is what keeps it from running on every keystroke.

field/custom-validation.tsx

Try “acme” — it is one of the names this demo rejects.

Checkboxes and switches

A control with no text of its own goes inside the label. The label then lays itself out as a row, takes a pointer cursor because clicking it now toggles the control, and any description or error below indents to line up with the label's text rather than with the box.

field/checkbox-and-switch.tsx

Required before the workspace can be created.

At most one message a month.

With a Select

field/with-select.tsx
Deploy region

Data never leaves the region you pick.

Groups

Field.Item gives one member of a checkbox or radio group its own label and description without opening a second field. The outer Field.Root still names the group as a whole.

field/items.tsx
Token scopes

List and fetch every resource in the workspace.

Create and update resources, but never delete them.

Manage members, billing and API keys.

State

Every part carries the field's state on data-*, so a consumer can style off it from plain CSS or a Tailwind arbitrary variant (data-[invalid]:...):

AttributeMeaning
data-valid / data-invalidThe last validation run passed / failed. Neither is present before the field has been validated at all.
data-dirtyThe value differs from the one it mounted with.
data-touchedThe control has been focused and blurred.
data-filledThe control has a value.
data-focusedThe control has focus.
data-disabledFrom Field.Root, or from an enclosing Fieldset.Root.

The library styles only two of them. data-invalid turns the control's boundary danger-coloured, and data-disabled dims the whole field. The rest describe where the user is in the interaction, and recolouring a control the moment it is merely touched is noise — they are exposed for you to use, not used by default.

Accessibility

The description and the error are both registered into the control's aria-describedby, and the error's id is removed again when the message unmounts — so a screen reader never announces a stale error, and never announces a description that is no longer there.

Enter and exit are transitions on [data-starting-style] / [data-ending-style], not keyframes, so a message that appears and is corrected quickly reverses mid-flight instead of snapping. Base UI keeps the outgoing element mounted until the exit finishes, which is what lets the old text fade out instead of being replaced by the new one.

In forced-colors mode every colour in the field collapses to CanvasText, so the error message loses the one thing marking it as an error. Colour cannot recover it and forced-color-adjust: none would opt the text out of the user's palette entirely — so the distinction is redrawn as an inline-start border, which forced-colors does preserve.

Theming

--forte-field-gap and --forte-field-indent are declared on Field.Root and read by the parts below it, so one value on the root moves the whole field. The colour and font knobs stay on the part that paints them, so recolouring the error does not reach the description.

Theming tokens for Field
PropertyDeclared onControlsDefault
--forte-field-gapField.RootVertical rhythm between label, control, description and errorvar(--forte-space-2)
--forte-field-indentField.RootIndent for a description or error under a wrapping label, so it lines up with the label text rather than the checkboxcalc(var(--forte-space-4) + var(--forte-control-gap))
--forte-field-label-fgField.LabelLabel colourvar(--forte-color-foreground)
--forte-field-label-font-sizeField.LabelLabel font sizevar(--forte-font-size-2)
--forte-field-label-font-weightField.LabelLabel font weightvar(--forte-font-weight-medium)
--forte-field-label-gapField.LabelGap between a wrapped control and its label textvar(--forte-control-gap)
--forte-field-description-fgField.DescriptionDescription colourvar(--forte-color-foreground-muted)
--forte-field-description-font-sizeField.DescriptionDescription font sizevar(--forte-font-size-1)
--forte-field-error-fgField.ErrorError colourvar(--forte-color-danger-text)
--forte-field-error-font-sizeField.ErrorError font sizevar(--forte-font-size-1)

--forte-field-indent defaults to an md Checkbox's width plus the label gap. It has to be a value rather than a measurement — the control declares its own size on itself, and a custom property set on a descendant cannot be read from an ancestor — so a field built on sm or lg controls wants it set explicitly.

API reference

Field.Root

Props for FieldRoot
PropTypeDefaultDescription
classNamestringAdditional class name(s). Applied after the internal styles so consumer utilities (e.g. Tailwind) win without needing `!important`.

It also forwards Base UI's validate, validationMode, validationDebounceTime, invalid, dirty, touched and actionsRef.

Field.Label

Props for FieldLabel
PropTypeDefaultDescription
classNamestringAdditional class name(s). Applied after the internal styles so consumer utilities (e.g. Tailwind) win without needing `!important`.
nativeLabelbooleantrueWhether the rendered element is a native `<label>`. Leave it `true` for `Input`, `Checkbox` and `Switch`. Set it to `false` for a control that is a `<button>` — `Select.Trigger` is the one in this library — where native label behaviour is actively wrong: the button would pick up `:hover` from the label, and clicking the label would fire a click on the trigger and open the popup. With `false` the association is made with `aria-labelledby` instead, and the element rendered defaults to a `<div>` so the pair stays consistent (Base UI logs an error in development if a `<label>` is rendered with `nativeLabel={false}`, or vice versa).

Field.Description

Props for FieldDescription
PropTypeDefaultDescription
classNamestringAdditional class name(s). Applied after the internal styles so consumer utilities (e.g. Tailwind) win without needing `!important`.

Field.Error

Props for FieldError
PropTypeDefaultDescription
childrenReactNodeThe message. Omit it and the field supplies the text itself; several failures at once arrive as a `<ul>`.
classNamestringAdditional class name(s). Applied after the internal styles so consumer utilities (e.g. Tailwind) win without needing `!important`.
matchboolean | keyof ValidityStateWhich failure to render for. Omit it to show whatever message the field currently has — the browser's own text for a native constraint, the string returned by `validate`, or the entry `<Form errors>` holds for this field's `name`. Pass a `ValidityState` key (`"valueMissing"`, `"typeMismatch"`, `"patternMismatch"`, …) to render only for that one failure, which is how you replace a browser message with your own wording. Pass `true` to always render, when an external form library owns visibility.

Field.Item

Props for FieldItem
PropTypeDefaultDescription
classNamestringAdditional class name(s). Applied after the internal styles so consumer utilities (e.g. Tailwind) win without needing `!important`.
disabledbooleanfalseWhether this one item ignores user interaction. `disabled` on the surrounding `Field.Root` takes precedence.

Field.Validity

Re-exported from Base UI unchanged. It renders no DOM of its own — children is a function that receives the raw ValidityState along with the field's value and error, for the cases where a message is not the right output.

<Field.Validity>
  {(state) => (
    <progress value={String(state.value ?? "").length} max={12} />
  )}
</Field.Validity>