Currently, AgnosticUI form components (e.g. AgInput, AgCheckbox) include <label> inside the shadow DOM as a pragmatic a11y solution for the label association problem. However, this does not address full form participation — meaning these components don't yet natively integrate with a parent <form> element for value submission, constraint validation, required/disabled handling, and reset() support.
The goal is to investigate and implement Form Associated Custom Elements (FACE) via static formAssociated = true and the ElementInternals API (attachInternals()) across relevant form components.
What full implementation would unlock:
- Component values submitted with parent
<form> natively
- Constraint validation (
required, custom validity messages)
disabled and readonly participation
form.reset() support
Known complexity:
ElementInternals requires manual wiring of behaviors that native inputs get for free — it's not automatic even after setting formAssociated
- Built-in form behaviors like
required, disabled, and constraint validation have to be explicitly implemented via ElementInternals rather than inherited naturally
- The mental model mismatch: developers expect web components to "just work" like native inputs inside forms, but you're essentially reimplementing native form control behavior from scratch
- Note: browser support is no longer a concern — as of January 2026 FACE has ~95% global coverage including Safari (supported since 16.4). No polyfill strategy needed.
Cheat sheet:
import { LitElement, html } from 'lit';
import { customElement, property } from 'lit/decorators.js';
@customElement('my-input')
export class MyInput extends LitElement {
static formAssociated = true;
private _internals: ElementInternals;
constructor() {
super();
this._internals = this.attachInternals();
}
@property()
value = '';
// Sync value to parent form whenever internal state changes
handleInput(e: Event) {
this.value = (e.target as HTMLInputElement).value;
this._internals.setFormValue(this.value);
}
// Called automatically when parent form is reset
formResetCallback() {
this.value = '';
this._internals.setFormValue('');
}
// Custom validity example
validate() {
if (!this.value) {
// This is bothersome. We'll need to investigate how to invert control to consumer. If not full IOC we need
// to think through what control CAN we provide to consumers. Obviously, this would break i18n locale control
// and takes away flexibility the consumer developer likely wants.
this._internals.setValidity({ valueMissing: true }, 'This field is required');
} else {
this._internals.setValidity({});
}
}
// Custom State Set for CSS pseudo-classes e.g. :--checked, :--loading
setCustomState(state: string, active: boolean) {
active ? this._internals.states.add(state) : this._internals.states.delete(state);
}
// Allow access to parent form; one of several boiler-plate get/set to provide (name, type, etc.) some of which will be
// component specific e.g. a Checkbox would need: get checked() { return this.hasAttribute('checked'); }
// and also set checked(flag), click handling, and so on.
get form() { return this._internals.form; }
render() {
return html`<input .value=${this.value} @input=${this.handleInput}>`;
}
}
Scope: AgInput only. Other components tracked in the planning doc
this issue produces. Playbook is documented but not fully implemented.
Acceptance criteria:
###STOP###
The LLM should ignore the following and it's not necessary for the LLM to visit these links as they're more for background information on how FACE works.
Research:
Currently, AgnosticUI form components (e.g.
AgInput,AgCheckbox) include<label>inside the shadow DOM as a pragmatic a11y solution for the label association problem. However, this does not address full form participation — meaning these components don't yet natively integrate with a parent<form>element for value submission, constraint validation,required/disabledhandling, andreset()support.The goal is to investigate and implement Form Associated Custom Elements (FACE) via
static formAssociated = trueand theElementInternalsAPI (attachInternals()) across relevant form components.What full implementation would unlock:
<form>nativelyrequired, custom validity messages)disabledandreadonlyparticipationform.reset()supportKnown complexity:
ElementInternalsrequires manual wiring of behaviors that native inputs get for free — it's not automatic even after settingformAssociatedrequired,disabled, and constraint validation have to be explicitly implemented viaElementInternalsrather than inherited naturallyCheat sheet:
Scope: AgInput only. Other components tracked in the planning doc
this issue produces. Playbook is documented but not fully implemented.
Acceptance criteria:
AgInputparticipates fully in parent form submissionrequired,minlength, custom validity messages)disabledpropagates correctlyform.reset()restores default valueissue. Deliverable is PROMPT files only (following the pattern in
v2/playbooks/README.md) — no generated react-example/, vue-example/, or
lit-example/ output. The Playbook should demonstrate looping through form
controls to validate an entire form using the FACE-enabled components
produced by this and follow-up issues.
###STOP###
The LLM should ignore the following and it's not necessary for the LLM to visit these links as they're more for background information on how FACE works.
Research: