Skip to content

Form Field

Form InfrastructureSince 0.2.0
import { FormField } from '@absoluteui/components/form-field';
import '@absoluteui/components/form-field/style.css';

Using Angular? See Form Field for Angular.

Field chrome and validation for a consumer-owned native <input>, <textarea>, or <select>. FormField adds the label, required indicator, help text, and validation message, maps the browser’s ValidityState to AbsoluteUI’s localized messages, applies the shared message timing, and registers the field with Form. The control element stays yours: it is not cloned or re-created, and only its ARIA wiring and the surrounding chrome are managed.

  • Native controls AbsoluteUI does not render itself: type=number, text-like input types (text, email, url, tel, search, password), a native <select> or <select multiple>, or a <textarea> with native attributes.
  • Progressive enhancement of server-rendered inputs that must keep their element and attributes.
  • Making a plain native element take part in Form validation and the error summary.
  • Text entry with counters, addons, or password reveal: use Text Input or Textarea.
  • Checkboxes, radios, and switches: use Checkbox, Radio Group, or Switch (Form Field rejects them).
  • Date and time values: use Date Input, Date Picker, or Time Picker (date, time, datetime-local, month, and week inputs are rejected).
  • Range sliders and color pickers: range and color inputs are rejected.
  • File uploads: use File Upload.
import { FormField } from '@absoluteui/components/form-field';
const control = document.createElement('input');
control.name = 'zip';
control.inputMode = 'numeric';
control.autocomplete = 'postal-code';
control.pattern = '[0-9]{5}';
const host = document.createElement('div');
document.body.append(host);
const zip = FormField.create(host, { label: 'ZIP code', control, required: true, description: 'Five digits' });
host.addEventListener('aui-change', () => {
host.dataset.valid = String(zip.validity.valid);
});

Native select and textarea with consumer validation

Section titled “Native select and textarea with consumer validation”
import { FormField, type FormFieldChangeDetail } from '@absoluteui/components/form-field';
const topic = document.createElement('select');
topic.name = 'topic';
topic.add(new Option('Choose a topic', ''));
topic.add(new Option('Billing', 'billing'));
topic.add(new Option('Technical support', 'support'));
const topicHost = document.createElement('div');
document.body.append(topicHost);
FormField.create(topicHost, { label: 'Topic', control: topic, required: true });
const details = document.createElement('textarea');
details.name = 'details';
details.maxLength = 500;
const detailsHost = document.createElement('div');
document.body.append(detailsHost);
FormField.create(detailsHost, {
label: 'Details',
control: details,
validate: (value) => (typeof value === 'string' && /https?:\/\//.test(value) ? 'Remove links from your message.' : null),
});
detailsHost.addEventListener('aui-input', (event) => {
const { validity } = (event as CustomEvent<FormFieldChangeDetail>).detail;
detailsHost.dataset.valid = String(validity.valid);
});

Enhance server-rendered markup and refresh after programmatic changes

Section titled “Enhance server-rendered markup and refresh after programmatic changes”
import { FormField } from '@absoluteui/components/form-field';
const host = document.querySelector<HTMLElement>('#quantity-field');
const control = host?.querySelector<HTMLInputElement>('input[type="number"]');
if (host && control) {
const quantity = FormField.create(host, { label: 'Quantity', control });
// Native attributes are the constraints; re-read them after changing them in code.
control.max = '10';
control.value = '12';
quantity.refresh();
quantity.reportValidity();
}
import { configureAbsoluteUI } from '@absoluteui/core';
import '@absoluteui/components/form-field';
configureAbsoluteUI({ components: { formField: { validationTiming: 'input' } } });
import { FormField } from '@absoluteui/components/form-field';
import '@absoluteui/components/form-field/style.css';
const host = document.querySelector<HTMLElement>('#guests-field');
const control = host?.querySelector<HTMLInputElement>('input[name="guests"]');
if (host && control) {
const guests = FormField.create(host, { label: 'Number of guests', control, required: true, description: 'Up to 8 people' });
host.addEventListener('aui-change', () => {
if (!guests.validity.valid) guests.reportValidity();
});
}
import { FormField } from '@absoluteui/components/form-field';
const control = document.createElement('input');
control.name = 'zip';
control.pattern = '[0-9]{5}';
const host = document.createElement('div');
document.body.append(host);
FormField.create(host, { label: 'ZIP code', control, required: true, description: 'Five digits' });
<div class="aui-field aui-form-field" data-aui-control> (consumer host; children replaced)
<label class="aui-field__label" for=control>Label<span class="aui-field__required" aria-hidden="true">*</span></label>
<input|textarea|select class="aui-form-field__control"> (consumer's element, moved into the host)
<div class="aui-field__description"> (optional)
<div class="aui-field__error aui-validation-message"> (when shown)
</div>
FormField.create(host: HTMLElement, options: FormFieldOptions): FormField

Global configuration key: formField (see Configuration).

Option Type Default Description
label string Visible label and accessible name (<label for> the control). Required.
control HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement The native control to wrap; moved into the host. It receives an id if it has none. Choice, file, hidden, button, date/time (date, time, datetime-local, month, week), range, and color input types are rejected. Required. Set at creation only.
hideLabel boolean false Visually hide the label; it remains the accessible name.
description string Help text referenced by aria-describedby.
required boolean Sets the native required property and the indicator. When omitted, the control’s own required attribute is used.
validate (value: string | string[]) => string | null Consumer validation after native constraints; return a message to mark the value invalid. string[] for <select multiple>.
icons Partial<{ error: IconRef }> Icon slot overrides.
validationTiming 'blur' | 'input' | 'submit' "blur" When validation messages first appear; afterwards they update live. Globally configurable.
locale string "en-US" Message and number-formatting locale. Globally configurable. Inherited from common configuration.
Property Type Description
host HTMLElement Consumer host; its children are replaced by the chrome and the control.
control FormFieldControl The wrapped native control.
value string | string[] Current value; string[] for <select multiple>.
name string | null The control’s name, or null when empty.
label string Label text.
disabled boolean The control’s disabled property.
validity FieldValidity { valid, error, message }; recomputed on each render.
dirty boolean Value differs from the value at creation.
touched boolean Focus has left the control at least once.
Method Description
refresh(): void Re-read native constraints, setCustomValidity() state, and the value after changing them in code.
update(options: Partial<Omit<FormFieldOptions, 'control'>>): void Merge and apply options.
reportValidity(): boolean Show messages now (marks the field submitted) and return validity.
setCustomError(message: string | null): void External error (server, cross-field rule); cleared when the user types. null or ‘’ clears it.
getFormValue(): string | string[] Value for form serialization.
focus(): void Focus the control.
reset(): void Restore the value (or selected options) captured at creation and clear dirty/touched/submitted/custom-error state.
destroy(): void Remove the chrome, listeners, registration, and subscription; restore the consumer’s aria-describedby. The control stays in the host.
Event Detail Bubbles Cancelable Description
aui-input { value: string | string[]; validity: FieldValidity } Yes No Native input event on the control.
aui-change { value: string | string[]; validity: FieldValidity } Yes No Native change event on the control.
Area Description
control The consumer’s native element, placed between the label and the help text.

All rules live in the aui.components cascade layer, so application styles override them without specificity battles.

Class Purpose
aui-field aui-form-field Host.
aui-form-field--invalid Host modifier while a message is shown.
aui-field__label | __required | __description | __error Shared field chrome.
aui-form-field__control Added to the native control: AbsoluteUI control surface.
Slot Default icon
error alert-circle

Tokens this stylesheet reads (verified in CI). Override them in a theme or with the Theme Designer.

--aui-border-width | --aui-color-border | --aui-color-border-strong | --aui-color-disabled-bg | --aui-color-error-fg | --aui-color-focus-ring | --aui-color-surface | --aui-color-surface-sunken | --aui-color-text | --aui-color-text-disabled | --aui-color-text-muted | --aui-control-height-md | --aui-control-padding-x | --aui-duration-fast | --aui-easing-standard | --aui-focus-ring-width | --aui-font-family-sans | --aui-font-size-md | --aui-font-size-sm | --aui-font-weight-medium | --aui-icon-size-sm | --aui-line-height-normal | --aui-line-height-tight | --aui-radius-md | --aui-space-1 | --aui-space-2

type FormFieldControl = HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement;
type FormFieldValue = string | string[];
interface FormFieldIcons { error: IconRef }
interface FormFieldChangeDetail { value: FormFieldValue; validity: FieldValidity }
interface FormFieldOptions {
label: string;
control: FormFieldControl;
hideLabel?: boolean;
description?: string;
required?: boolean;
validate?: (value: FormFieldValue) => string | null;
validationTiming?: ValidationTiming;
locale?: string;
icons?: Partial<FormFieldIcons>;
}

Pattern: Native form control with a <label for> association (HTML); the consumer’s element keeps its native role.

Key Context Action
Tab / Shift+Tab Any Moves focus to and from the control.
All native keys Control focused Native behavior of the element (editing, arrow stepping, select picker).
Element Role
control Native role of the wrapped element (textbox, spinbutton, combobox/listbox for <select>, …)
error icon none (aria-hidden=true)
Attribute Element Value
aria-describedby control Help text and visible error ids, followed by ids the consumer had set before creation
aria-invalid control “true” only while an error message is shown
required control Native attribute, set from the required option when given

The control shows the shared control focus indicator (border in color-focus-ring plus a ring); forced-colors mode uses a Highlight outline.

Criterion Level Note
1.3.1 Info and Relationships A
1.4.3 Contrast (Minimum) AA
1.4.11 Non-text Contrast AA
2.1.1 Keyboard A
2.4.7 Focus Visible AA
2.5.3 Label in Name A
3.3.1 Error Identification A
3.3.2 Labels or Instructions A
3.3.3 Error Suggestion AA
4.1.2 Name, Role, Value A

Automated test results: Accessibility and compliance.

  • The required asterisk is aria-hidden; the native required attribute conveys the state.
  • Native validation bubbles are suppressed (the invalid event is prevented) so only the AbsoluteUI message is shown and read.
  • Errors appear after the first blur by default; when focus left because a pointer was pressed elsewhere, the message renders after the pointer is released so the click is not displaced.
  • Existing aria-describedby references (for example an external hint) are kept and restored on destroy.
  • Behavior with every native input type has not been manually verified with NVDA, JAWS, and VoiceOver.

default, focus-visible, filled, disabled, read-only, required, invalid (message shown).

From Trigger To
pristine user edits dirty; message shown only with validationTiming: 'input'
any control blurs touched; messages shown (blur timing), deferred until pointer release when a pointer caused the blur
any reportValidity() (for example on form submit) submitted; messages shown for every timing
invalid (custom error) user types custom error cleared, validity recomputed
any reset() value at creation; pristine, untouched
  • The required asterisk is aria-hidden; the native required attribute conveys the state.
  • Native validation bubbles are suppressed (the invalid event is prevented), so only the AbsoluteUI message is shown and read.
  • Errors appear after the first blur by default. When the blur was caused by pressing a pointer elsewhere, the message renders after the pointer is released so a click on Submit is not displaced.
  • Missing values on a <select> use “Select an option.”; text-like controls use “This field is required.”
  • Manual screen-reader verification is pending (see Known limitations).

The control fills its container (inline-size: 100%, min-inline-size: 0). Textareas resize vertically. Density tokens change the control height.

Follows the document direction; the chrome uses logical properties.

Role tokens through the shared control-surface and text-control-states mixins; a <select> keeps its native picker and only the closed control is styled. In forced-colors mode, focus draws a Highlight outline.

configureAbsoluteUI({ components: { formField: { validationTiming: 'input' } } });

Changes apply to existing instances immediately.

Instance options override global and theme defaults. All options except control are dynamic.

icons.error (default alert-circle).

All text uses textContent. Validation messages never include the entered value, only constraint parameters. The consumer’s element is not cloned, so its listeners and attributes survive. CSP-safe.

No persistence, network access, or telemetry. The native control carries its own value for form submission.

Four listeners on the control (input, change, blur, invalid) and one configuration subscription. Each event re-renders the chrome once. No dedicated performance budget.

One control per field. Unit tests cover each supported element type; large forms are expected to scale linearly.

  • Native minlength/maxlength apply only after user edits in browsers; Form Field also enforces them for programmatic values (counted in code points).
  • <select multiple> values are string[]; dirty compares the selected values with those at creation.
  • required omitted keeps the control’s own attribute; required: false removes it.
  • destroy() leaves the generated id and the required value on the control.
  • Numeric min, max, and step are read from the attributes; step="any" disables step checking. The step base follows HTML: min, else the value attribute, else 0.
  • A native setCustomValidity(message) takes precedence over every other check and yields error custom with the native validationMessage (the consumer validate callback does not run). Call setCustomValidity('') and then refresh() to clear it.
Code Category Cause
FORM_FIELD_OPTIONS configuration Missing or blank label; control is not an input, textarea, or select; or the input type is checkbox, radio, file, hidden, button, submit, reset, image, date, time, datetime-local, month, week, range, or color (use Checkbox, Radio Group, Switch, Date Input, Date Picker, or Time Picker).
Validation (validity.error) validation value-missing, type-mismatch, too-short, too-long, pattern-mismatch, bad-input, range-underflow, range-overflow, step-mismatch, custom.

A failed construction leaves the host and the control untouched.

  • After changing constraint attributes or the value in code, call refresh().
  • The initial value for dirty and reset() is captured at creation.
  • No character counter.
  • Screen-reader behavior across native input types has not yet been manually verified with NVDA, JAWS, and VoiceOver.
  • checkbox, radio, file, hidden, button, submit, reset, image, date, time, datetime-local, month, week, range, and color inputs.
  • More than one control per field.
  • HTML in the label, help text, or messages.