Form-Associated Custom Elements Guide
The Form-Associated Custom Elements (FACE) API bridges the gap
between Web Components and native HTML forms by allowing custom elements
to participate directly in form submission, validation, and lifecycle
events. Historically, custom controls required hidden
<input> elements or manual JavaScript overrides to
send data and trigger validation. This article explains how the FACE API
functions, how to enable it via the ElementInternals
interface, and how it handles values, native validation, and form state
changes seamlessly.
Enabling Form Association
To convert a standard Autonomous Custom Element into a form-associated control, you must declare a static property within the element class:
class CustomTextInput extends HTMLElement {
static formAssociated = true;
constructor() {
super();
this.internals_ = this.attachInternals();
}
}
customElements.define('custom-text-input', CustomTextInput);Setting static formAssociated = true tells the browser
that this element can interact with enclosing <form>
elements, be identified by the form attribute, and support
standard form behaviors. Calling this.attachInternals()
returns an ElementInternals instance, which provides the
methods required to communicate with the parent form.
Managing Form Value and Submission
The setFormValue() method on
ElementInternals assigns the value that the custom element
sends during form submission or when reading
new FormData(form).
- Single Value: Call
this.internals_.setFormValue(this.value)to submit a string,File, ornull. - Multiple Values: Pass a
FormDatainstance tosetFormValue()if your single custom element represents multiple fields (e.g., a date range picker submitting bothstartDateandendDate).
// Updating the value when internal state changes
this.internals_.setFormValue(this.currentValue);When the parent form is submitted, the browser automatically collects
the value defined by setFormValue() under the custom
element’s name attribute.
Implementing Native Constraint Validation
The FACE API brings native form validation to custom elements through
the setValidity() method on ElementInternals.
This allows custom components to react to native CSS pseudo-classes like
:valid, :invalid, and
:user-invalid, and participate in
form.checkValidity().
// Setting custom validation flags and messages
if (!this.value) {
this.internals_.setValidity(
{ valueMissing: true },
'This field cannot be empty.',
this.shadowRoot.querySelector('input') // Validation message anchor
);
} else {
this.internals_.setValidity({}); // Clear validation errors
}The ElementInternals instance exposes standard
validation properties and methods: * internals.validity: A
read-only ValidityState object. *
internals.validationMessage: The active error message. *
internals.checkValidity(): Triggers the
invalid event if invalid. *
internals.reportValidity(): Displays the browser’s native
validation popup.
Handling Form Lifecycle Callbacks
The API provides specific lifecycle hooks that the browser executes in response to form-level actions:
formAssociatedCallback(form): Triggered when the element is associated with or disassociated from a<form>element.formResetCallback(): Triggered when the user or script resets the form. The custom control should revert to its default value here.formDisabledCallback(disabled): Triggered when the element’s disabled state changes, including when a parent<fieldset>is disabled.formStateRestoreCallback(state, mode): Triggered when the browser restores the element’s state after navigation or autofill.
Accessibility and State Reflection
ElementInternals also allows custom controls to manage
Accessibility Object Model (AOM) attributes directly without cluttering
the host element with ARIA attributes:
this.internals_.ariaRequired = 'true';
this.internals_.ariaDisabled = String(this.disabled);By unifying data submission, constraint validation, lifecycle events,
and accessibility under a single native API, form-associated custom
elements behave identically to built-in form controls such as
<input>, <select>, and
<textarea>.