Skip to main content

A form field wrapper that provides accessible labeling, helper and error text, validation state and indicators for form inputs.

Choose a unique username for your account.
<primitives:field.root>
    <primitives:field.label />
    <primitives:field.control asChild="{true}">
        <!-- Your form input here -->
    </primitives:field.control>
    <primitives:field.indicator type="required" />
    <primitives:field.helperText />
    <primitives:field.errorText />
</primitives:field.root>
typo3 ui:add field

Read more about installing Components and Primitives.

Mark a field as required. An empty required field stays quiet until the user has edited it or the form is submitted.

Add helpful text below the input. It is linked to the control with aria-describedby.

Must be at least 8 characters long.

Indicate that the field has an error and display an error message.

The password must be at least 8 characters long.

Disable the entire field.

Use with the Checkbox component.

The field reads the ValidityState of its control, so the constraints you already know are validation rules: required (the prop of the field, not an attribute), type="email" and the like on the input, and pattern, min, max, step, minlength and maxlength, which you pass to the input part as plain attributes.

The form renders novalidate, so the browser never shows its own bubble. Instead, an error text without a match shows the message of the failed constraint, in the language of the browser. Give it a match to show your own text for one constraint, e.g. valueMissing, typeMismatch, patternMismatch, tooShort, tooLong, rangeUnderflow, rangeOverflow, stepMismatch, badInput or customError. An error text with a match stays hidden until exactly that constraint failed.

Five digits.

validationMode decides when a field validates and shows its errors:

In every mode, an error that is already showing is validated again on every change, so the message goes away as soon as the user has fixed the value, instead of waiting for the next blur. A submit validates all fields, shows every error and moves the focus to the first invalid one.

Error texts and indicators are never shown or hidden while a mouse button or a finger is down. A field blurs on the press that leaves it, and an error text appearing at that moment would move the button the user is about to release on, so the click would be lost. They wait for the release instead (keyboard users get them right away).

Errors show after the first submit.
Errors show when you leave an edited field.
Errors show while you type.

An indicator shows its content while the field is in one state, and stays hidden otherwise. Its type is one of required, invalid, valid and validating. valid appears once the field has been validated and passed.

At least three lowercase letters, digits or underscores.

A field can run its own check, for example to ask the server whether a username is still free. Pass a validate function that returns a message (or a list of messages) to mark the field invalid, and null to accept the value. It may return a promise.

A function cannot travel through the server-rendered markup, so pass it from TypeScript: render the field with autoMount="{false}", and mount it yourself. While the promise is pending the field is validating (use the validating indicator), a result for a value the user has changed meanwhile is dropped, and a submit waits for it.

validate receives the value and a snapshot of the native validity. It runs at the same points that make an error visible (see the validation mode), not on every keystroke, so it is a good place for a request.

Try "admin" or "ada".

Use updateProps({ validate }) to replace the function later. The schema or callback of the Form and the errors the server sent back (422 or from onSubmit) do not need any of this, they reach the field on their own and are shown in the same error text.

A field can react to a different field's value changing via the listenTo prop - names of sibling fields within the same form to watch. Two things happen whenever a listened-to field changes its value: a fluid-primitives:field:dependencychange event is dispatched on the root element of this field, for any imperative DOM reaction you need, and this field is treated as if its own value had changed. It validates again under the same rules as when its own user types: immediately if it shows an error (or its validationMode is onChange, or the form was submitted before), otherwise at its next blur or submit.

FieldHandle (what both form.api.getField(name) and a Field instance's own .api return) exposes addDependencyChangeListener(callback) for the first half - it registers callback and returns a function that removes it again, the same "call it to start listening, get a cleanup function back" shape as addEventListener/Machine.subscribe, rather than a config-style onX prop you'd set once:

const needsStudentId = (values: FormValues) => values.get('ticketType') === 'student';
const studentIdField = form.api.getField('studentId')!;

const applyStudentIdVisibility = (show: boolean) => {
    studentIdField.getRootEl()!.hidden = !show;
    studentIdField.setDisabled(!show);
};

// the same rule decides the starting state and every later change
applyStudentIdVisibility(needsStudentId(form.api.getValues()));
studentIdField.addDependencyChangeListener(({ values }) => {
    applyStudentIdVisibility(needsStudentId(values));
});

The callback receives dependencies (the current value of each listened-to field, keyed by name) and values, the current FormValues of the whole form, which is what a rule shared with your validation callback usually wants.

See the Complete Example: Event Registration in the Forms guide for this exact pattern used to show/hide a studentId field based on ticketType, in place of recomputing it inside the form's own render callback on every field change.

The classic case is a password confirmation field - passwordConfirm listens to password, so changing password validates passwordConfirm again, instead of leaving a stale result:

<ui:field.root name="password">
    <ui:input.root type="password">
        <ui:input.label>Password</ui:input.label>
        <ui:input.input />
    </ui:input.root>
    <ui:field.errorText />
</ui:field.root>

<ui:field.root name="passwordConfirm" listenTo="{0: 'password'}">
    <ui:input.root type="password">
        <ui:input.label>Confirm password</ui:input.label>
        <ui:input.input />
    </ui:input.root>
    <ui:field.errorText />
</ui:field.root>

No consumer JavaScript is needed for the re-triggering itself - listenTo handles that. You still need to decide what "valid" means, though, and a manual validation callback (see Manual Client-Side Validation) fits better here than a single schema: checking each field independently means passwordConfirm's mismatch error can appear even while password hasn't yet satisfied its own length requirement, whereas a single Zod schema with .refine() would silently skip that cross-field check until the base shape (including password's own min(8)) validates first:

validation: ({ values }) => {
    const errors: Record<string, { messages: string[] }> = {};

    const password = values.get('password');
    if (typeof password !== 'string' || password.length < 8) {
        errors.password = { messages: ['Password must be at least 8 characters'] };
    }

    const passwordConfirm = values.get('passwordConfirm');
    if (passwordConfirm !== null && passwordConfirm !== password) {
        errors.passwordConfirm = { messages: ['Passwords do not match'] };
    }

    return errors;
},
At least 8 characters.

Every part of a field renders the state of the field as data attributes, so a label, an indicator or an error text can be styled without any JavaScript. The server renders the ones that follow from the props (data-disabled, data-invalid, data-required, data-readonly), the others are set once the field is hydrated. The API reference lists them per part.

The same flags are available from TypeScript on field.api (and on the FieldHandle of form.api.getField(name)): touched, dirty, filled, focused, valid, invalid, validating, disabled, required, readOnly and the errors list, together with validate(), clearErrors() and reset(). A fluid-primitives:field:valuechange event bubbles from the root of the field whenever its value settled on a new one, 100 ms after the last keystroke by default.

The following tables cover the available props of the Fluid Primitives.

Provides shared field state for labels, helper text, errors, indicators and controls. Renders a <div> element.

NameDescriptionRequiredDefault
namestring
The unique field name used to associate labels, helper text, errors, and form values.
Yes
-
disabledboolean
Whether the field is disabled. A field inside a disabled `<fieldset>` is disabled as well.
No
-
invalidboolean
Whether the field is invalid, regardless of validation. Use it for errors the browser's constraint validation and the form's validation cannot compute, e.g. a pre-populated server error.
No
-
requiredboolean
Whether the field is required. An empty required field stays quiet until the user edits it or the form is submitted.
No
-
readOnlyboolean
Whether the field is read-only.
No
-
defaultValuemixed
The initial value that child controls can use when rendered inside the field.
No
-
validationModeEnum\ValidationMode
When errors become visible. `onBlur` validates on blur once the value was edited, `onSubmit` after the first submit attempt, `onChange` on every change. An error that is showing always revalidates on change, in every mode.
No
OnBlur
listenTostring[]
Names of sibling fields within the same form whose value changes this field should react to. Dispatches a `fluid-primitives:field:dependencychange` event on this field's root element (see `addDependencyChangeListener()`) on every value change of a listened-to field, and treats that like a change of this field itself, so it revalidates under the same rules as when its own value changes - e.g. a `passwordConfirm` field listening to `password`.
No
-
attributesarray
Additional attributes that should be rendered on the component where ui:attributes is used.
No
[]
asChildboolean
If true the component uses its child only without the component template. Like Radix UI asChild or Base UI render props.
No
-
rootIdstring
The root ID of the component, used for hydration and identification.
No
-
idsarray
The IDs of of the component parts for composition.
No
[]
autoMountboolean
Whether the client initializes the component on its own. Set it to false to mount it yourself with `mount()`
No
true
classstring
The CSS class(es) to be applied to the component.
No
-

Rendered data attributes

AttributeDescription
data-disabledPresent when the field is disabled, or it is inside a disabled fieldset.
data-invalidPresent when the field is invalid: a native constraint failed, or `validate`, the form or the server reported an error.
data-validPresent when the field has been validated and passed.
data-requiredPresent when the field is required.
data-readonlyPresent when the field is read-only.
data-touchedPresent once the user has left the field at least once.
data-dirtyPresent when the value differs from the one the field started with. It goes away again when the user reverts the edit.
data-filledPresent when the field has a value.
data-focusPresent when focus is inside the field.
data-nameThe name of the field.

Labels the associated form control. Renders a <label> element.

NameDescriptionRequiredDefault
attributesarray
Additional attributes that should be rendered on the component where ui:attributes is used.
No
[]
asChildboolean
If true the component uses its child only without the component template. Like Radix UI asChild or Base UI render props.
No
-
classstring
The CSS class(es) to be applied to the component.
No
-

Rendered data attributes

AttributeDescription
data-disabledPresent when the field is disabled, or it is inside a disabled fieldset.
data-invalidPresent when the field is invalid: a native constraint failed, or `validate`, the form or the server reported an error.
data-validPresent when the field has been validated and passed.
data-requiredPresent when the field is required.
data-readonlyPresent when the field is read-only.
data-touchedPresent once the user has left the field at least once.
data-dirtyPresent when the value differs from the one the field started with. It goes away again when the user reverts the edit.
data-filledPresent when the field has a value.
data-focusPresent when focus is inside the field.

Wraps the slotted form control and wires up shared field attributes. Renders the element defined by the asChild prop.

NameDescriptionRequiredDefault
attributesarray
Additional attributes that should be rendered on the component where ui:attributes is used.
No
[]
asChildboolean
If true the component uses its child only without the component template. Like Radix UI asChild or Base UI render props.
No
-
classstring
The CSS class(es) to be applied to the component.
No
-

Rendered data attributes

AttributeDescription
data-disabledPresent when the field is disabled, or it is inside a disabled fieldset.
data-invalidPresent when the field is invalid: a native constraint failed, or `validate`, the form or the server reported an error.
data-validPresent when the field has been validated and passed.
data-requiredPresent when the field is required.
data-readonlyPresent when the field is read-only.
data-touchedPresent once the user has left the field at least once.
data-dirtyPresent when the value differs from the one the field started with. It goes away again when the user reverts the edit.
data-filledPresent when the field has a value.
data-focusPresent when focus is inside the field.

Displays help or supporting text for the field. Renders a <div> element.

NameDescriptionRequiredDefault
attributesarray
Additional attributes that should be rendered on the component where ui:attributes is used.
No
[]
asChildboolean
If true the component uses its child only without the component template. Like Radix UI asChild or Base UI render props.
No
-
classstring
The CSS class(es) to be applied to the component.
No
-

Rendered data attributes

AttributeDescription
data-disabledPresent when the field is disabled, or it is inside a disabled fieldset.
data-invalidPresent when the field is invalid: a native constraint failed, or `validate`, the form or the server reported an error.
data-validPresent when the field has been validated and passed.
data-requiredPresent when the field is required.
data-readonlyPresent when the field is read-only.
data-touchedPresent once the user has left the field at least once.
data-dirtyPresent when the value differs from the one the field started with. It goes away again when the user reverts the edit.
data-filledPresent when the field has a value.
data-focusPresent when focus is inside the field.

Displays the validation message of the field, or the text of its match when one constraint failed. Renders a <div> element.

NameDescriptionRequiredDefault
matchEnum\ValidityMatch
Narrows the error text to one failed native constraint, e.g. `valueMissing` or `typeMismatch`. Without it the text shows whenever the field is invalid and displays the error messages; with it the slotted text is shown as is.
No
-
attributesarray
Additional attributes that should be rendered on the component where ui:attributes is used.
No
[]
asChildboolean
If true the component uses its child only without the component template. Like Radix UI asChild or Base UI render props.
No
-
classstring
The CSS class(es) to be applied to the component.
No
-

Rendered data attributes

AttributeDescription
data-disabledPresent when the field is disabled, or it is inside a disabled fieldset.
data-invalidPresent when the field is invalid: a native constraint failed, or `validate`, the form or the server reported an error.
data-validPresent when the field has been validated and passed.
data-requiredPresent when the field is required.
data-readonlyPresent when the field is read-only.
data-touchedPresent once the user has left the field at least once.
data-dirtyPresent when the value differs from the one the field started with. It goes away again when the user reverts the edit.
data-filledPresent when the field has a value.
data-focusPresent when focus is inside the field.

Shows its content while the field is in the state given by type: required, invalid, valid or validating. Renders a <span> element.

NameDescriptionRequiredDefault
typeEnum\FieldIndicatorType
The state the indicator reflects: `required`, `invalid`, `valid` or `validating`. It stays in the DOM and is hidden unless the state matches, so CSS transitions survive.
Yes
-
attributesarray
Additional attributes that should be rendered on the component where ui:attributes is used.
No
[]
asChildboolean
If true the component uses its child only without the component template. Like Radix UI asChild or Base UI render props.
No
-
classstring
The CSS class(es) to be applied to the component.
No
-

Rendered data attributes

AttributeDescription
data-disabledPresent when the field is disabled, or it is inside a disabled fieldset.
data-invalidPresent when the field is invalid: a native constraint failed, or `validate`, the form or the server reported an error.
data-validPresent when the field has been validated and passed.
data-requiredPresent when the field is required.
data-readonlyPresent when the field is read-only.
data-touchedPresent once the user has left the field at least once.
data-dirtyPresent when the value differs from the one the field started with. It goes away again when the user reverts the edit.
data-filledPresent when the field has a value.
data-focusPresent when focus is inside the field.
data-typeThe state the indicator reflects. One of: "required", "invalid", "valid", "validating".
NameDescriptionDefault
namestring
The name of the field. It keys the field in its form and is what the field's value is read by.
-
defaultValueunknown | undefined
The initial value, used while the DOM holds no control for this name yet.
-
listenTostring[] | undefined
Names of sibling fields in the same form whose value changes this field reacts to.
-
idsElementIds | undefined
The ids of the elements. Useful for composition.
-
disabledboolean | undefined
Whether the field is disabled. Also inherited from a surrounding native <fieldset disabled>.
invalidboolean | undefined
Whether the field is invalid, regardless of computed validity. Use for app-level errors (e.g. server errors) the machine cannot compute itself.
-
dirtyboolean | undefined
Whether the field's value has been changed from its initial value. Useful when the field state is controlled by an external library.
-
touchedboolean | undefined
Whether the field has been touched. Useful when the field state is controlled by an external library.
-
readOnlyboolean | undefined
Whether the field is read-only.
requiredboolean | undefined
Whether the field is required.
validate((details: ValidateDetails) => ValidateResult | Promise<ValidateResult>) | undefined
Custom validation, run against the value and the native validity snapshot. May return a promise; a newer validation run supersedes an in-flight one.
-
validationModeValidationMode | undefined
When validation results become visible.
"onBlur"
onValidityChange((details: ValidityChangeDetails) => void) | undefined
Called when the committed validity changes.
-
dir"ltr" | "rtl" | undefined
The document's text/writing direction.
"ltr"
idstring
The unique identifier of the machine.
-
getRootNode(() => ShadowRoot | Document | Node) | undefined
A root node to correctly resolve document in custom environments. E.x.: Iframes, Electron.
-
NameTypeDescription
idsResolvedElementIdsThe resolved element ids, for composition with other components.
disabledbooleanWhether the field is disabled (including inherited fieldset disabled state).
invalidbooleanWhether the field is invalid.
validboolean | nullCommitted validity: null until validation has run.
requiredbooleanWhether the field is required.
readOnlybooleanWhether the field is read-only.
focusedbooleanWhether the control is focused.
touchedbooleanWhether the control has been blurred at least once.
dirtybooleanWhether the value differs from the initial value.
filledbooleanWhether the control has a non-empty value.
validatingbooleanWhether an async validation is in flight.
errorsstring[]The committed error messages.
validityValiditySnapshot | nullThe committed validity snapshot, null until validation has run.
ariaDescribedbystring | undefinedThe composed aria-describedby, for wiring custom controls.
validateVoidFunctionRuns validation now, surfacing valueMissing even on a pristine field.
clearErrorsVoidFunctionClears committed errors and returns validity to the pristine (unvalidated) state.
resetVoidFunctionResets interaction state and validity to pristine.
getRootState() => RootStateReturns the state of the root.
getLabelState() => LabelStateReturns the state of the label.
getControlState() => ControlStateReturns the state of the control (input, textarea, select, or a custom host).
getHelperTextState() => HelperTextStateReturns the state of the helper text.
getErrorTextState(props?: ErrorTextProps) => ErrorTextStateReturns the state of the error text.
getIndicatorState(props: IndicatorProps) => IndicatorStateReturns the state of the indicator.
namestringThe name of the field.
getValue() => FieldValueThe current value of the field.
getErrorText() => string | nullThe first error message of the field, null if there is none.
getRootEl() => HTMLElement | nullThe root element of the field.
getFormMachine() => FormMachine | undefinedThe machine of the form the field sits in, undefined outside of a form.
setDisabled(disabled: boolean) => voidDisables or enables the field.
setRequired(required: boolean) => voidMarks the field as required or optional.
setReadOnly(readOnly: boolean) => voidMakes the field read-only or editable.
addDependencyChangeListener(callback: (detail: FieldDependencyChangeDetail) => void) => () => voidCalls callback whenever the value of a field named in the listenTo prop of this field changes. Returns a function that removes the listener again. For a field without listenTo it never fires, but the returned function is still safe to call.