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).

// 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:


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>.