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.
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.
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.
Required before the workspace can be created.
At most one message a month.
With a Select
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.
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]:...):
| Attribute | Meaning |
|---|---|
data-valid / data-invalid | The last validation run passed / failed. Neither is present before the field has been validated at all. |
data-dirty | The value differs from the one it mounted with. |
data-touched | The control has been focused and blurred. |
data-filled | The control has a value. |
data-focused | The control has focus. |
data-disabled | From 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.
| Property | Declared on | Controls | Default |
|---|---|---|---|
--forte-field-gap | Field.Root | Vertical rhythm between label, control, description and error | var(--forte-space-2) |
--forte-field-indent | Field.Root | Indent for a description or error under a wrapping label, so it lines up with the label text rather than the checkbox | calc(var(--forte-space-4) + var(--forte-control-gap)) |
--forte-field-label-fg | Field.Label | Label colour | var(--forte-color-foreground) |
--forte-field-label-font-size | Field.Label | Label font size | var(--forte-font-size-2) |
--forte-field-label-font-weight | Field.Label | Label font weight | var(--forte-font-weight-medium) |
--forte-field-label-gap | Field.Label | Gap between a wrapped control and its label text | var(--forte-control-gap) |
--forte-field-description-fg | Field.Description | Description colour | var(--forte-color-foreground-muted) |
--forte-field-description-font-size | Field.Description | Description font size | var(--forte-font-size-1) |
--forte-field-error-fg | Field.Error | Error colour | var(--forte-color-danger-text) |
--forte-field-error-font-size | Field.Error | Error font size | var(--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
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | Additional 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
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | Additional class name(s). Applied after the internal styles so consumer utilities (e.g. Tailwind) win without needing `!important`. | |
nativeLabel | boolean | true | Whether 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
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | Additional class name(s). Applied after the internal styles so consumer utilities (e.g. Tailwind) win without needing `!important`. |
Field.Error
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | The message. Omit it and the field supplies the text itself; several failures at once arrive as a `<ul>`. | |
className | string | Additional class name(s). Applied after the internal styles so consumer utilities (e.g. Tailwind) win without needing `!important`. | |
match | boolean | keyof ValidityState | Which 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
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | Additional class name(s). Applied after the internal styles so consumer utilities (e.g. Tailwind) win without needing `!important`. | |
disabled | boolean | false | Whether 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>