Skip to content

Form-Associated Elements

The Form-Associated Custom Elements API lets a custom element participate in a <form> exactly like a native input. The element reports its value, validity, and can be included in FormData and form.submit().

Without this API, a custom element placed inside a <form> is invisible to the browser’s form submission machinery. Values it holds are not serialised, and built-in validation does not apply to it. With the API, your element becomes a first-class form participant indistinguishable from a native <input>.

Two things are required: a static formAssociated = true property on the class, and a call to this.attachInternals() in the constructor.

class MyInput extends HTMLElement {
static formAssociated = true;
constructor() {
super();
this.internals = this.attachInternals();
}
}

The static property must be declared before attachInternals() is called — the browser reads it at construction time to decide whether to grant form-participation abilities.

Once you have the internals object, these are the key form-related methods:

  • internals.setFormValue(value) — sets the value included in FormData when the form is submitted.
  • internals.setValidity(flags, message, anchor) — controls the element’s validation state. Pass an empty object and an empty string to clear all errors.
  • internals.reportValidity() — triggers the browser’s built-in validation UI (the tooltip bubble), the same way a native input would.
  • internals.checkValidity() — returns a boolean indicating whether the element currently passes validation.

The internals.role and ARIA reflection properties are covered in the Accessibility lesson.

// Form-Associated Custom Elements API (Chrome 77+, Firefox 98+, Safari 16.4+)
// static formAssociated = true — must be on the class before attachInternals()
// this.internals = this.attachInternals()
//
// internals.setFormValue(value) — submittable value
// internals.setFormValue(value, state) — value + restore-state hint
// internals.setValidity({}, '') — clear validity errors
// internals.setValidity({ valueMissing: true }, 'Required', anchorEl)
// internals.reportValidity() — show browser validation UI
// internals.checkValidity() — boolean
// internals.form — the associated <form> element
// internals.labels — NodeList of associated <label>s
// internals.willValidate — boolean

The demo below feature-detects support for attachInternals and formAssociated. If the browser does not support them, a fallback message is shown instead.

Type into the custom input and watch the paragraph below the form reflect the value that would be submitted with the form.

OptionBenefitCost
Native form-associated (static formAssociated = true + ElementInternals)The custom element joins a <form> like a native control — validates, resets, and submits its value with the form with no extra JavaScriptOnly supported in reasonably recent browsers, and the ElementInternals API has some subtleties to learn
Polyfill / a native <input> hidden inside the shadow DOMBroader support in older browsersAdds DOM complexity — you have to sync values between the custom element and the hidden input yourself, risking accessibility issues if the sync slips
  • Forgetting to set static formAssociated = true — without this flag, attachInternals() doesn’t return an ElementInternals that can join a form, and setFormValue() has no effect, so the custom input’s value never gets submitted with the form.
  • Calling attachInternals() in connectedCallback instead of the constructor — the internals object must be created exactly once per element. Calling it again in connectedCallback (which can run more than once) throws, since attachInternals can’t be called twice.
  • Not calling setFormValue() every time the value changes — if you update the element’s internal state but forget to sync it to the form value via internals.setFormValue(), the form submits a stale or empty value instead of what the user just entered.

💡 Real-world examples

Adobe Photoshop on the web — the entire app is built with Lit-based Web Components, using Spectrum Web Components as its main UI library, showing a large production app can be built on custom elements as its foundation. (web.dev/ps-on-the-web)

Shoelace — form controls like <sl-input> and <sl-checkbox> participate in native <form> submission without using ElementInternals/form-associated custom elements at all — instead they intercept the form’s formdata and submit events. (shoelace.style)

What static property must be set to true to make a custom element form-associated?
Which method sets the value that will appear in FormData on form submit?
What should a form-associated element do when attachInternals is not available?