Form fields strategy demos
Interactive PoCs exploring how label and description connect across Shadow DOM
boundaries for textfield, checkbox,
progress bar, combobox, and
button components. Most demos keep the ARIA role on an inner
shadow DOM element; the host-role demos put the
role on the custom element host via ElementInternals instead.
Project README · Open in StackBlitz
Shadow DOM label and description
Label and description live inside the shadow DOM with
<slot name="label"> and
<slot name="description">.
Same-root aria-labelledby / aria-describedby IDs
wire the role element.
Host role via ElementInternals
The ARIA role lives on the custom element host (set through
ElementInternals), yet the label and description it references
live inside the host's own shadow DOM — component-owned
<span> elements each wrapping a
<slot name="label"> /
<slot name="help-text"> for the slotted text. A same-tree
aria-labelledby IDREF on the host can't reach into its shadow root,
so wiring uses internals.ariaLabelledByElements /
ariaDescribedByElements (with mirrored
internals.ariaLabel as a fallback). The light-label variant points
the host at page-level elements via host.ariaLabelledByElements.
See the ARIA ref controllers.
Light DOM label and description siblings
Label and description are plain light DOM siblings. Each component exposes
labelledby / describedby properties that resolve
element IDs and update ariaLabelledByElements /
ariaDescribedByElements on the inner role element
(cross-root element reference API).
Hybrid — slotted with light DOM fallback
Shadow label and description slots take priority when filled.
When empty, the component falls back to
ariaLabelledByElements pointing at light DOM siblings
via the labelledby / describedby properties.
A toggle button switches between modes live.
Form-associated custom elements
textfield-form uses static formAssociated = true
and ElementInternals.setFormValue() to participate in form
submission and reset alongside native inputs.
button-form calls
internals.form.requestSubmit() /
internals.form.reset().
An output section shows FormData entries from both custom
and native fields on every submission.
Coordinated radio group (RadioGroupController)
RadioGroupController composes
SelectionController (mutual exclusion),
FocusgroupNavigationController (roving tabindex + arrow
selection), and SlotAttributePropagationController (name
propagation), driving each radio's own
FieldAssociationController so exactly one value submits per
group. The demo also includes a side-by-side experiment swapping in
LiveSelectionController — see the
RadioGroupController section of the README for the composition
and the experiment's findings.
referenceTarget labelling
Components set shadowRoot.referenceTarget = 'role' so
that aria-labelledby / aria-describedby on the
host element resolve directly to the inner shadow element
— no LabellingController, no
ariaLabelledByElements wiring. Requires enabling a browser flag
(instructions on the demo page). Compare with the
hybrid demo, which achieves the same result
today using ariaLabelledByElements.
Platform-Provided Behaviors shim
ButtonAssociationController is a shim for the proposed
Platform-Provided Behaviors spec (WHATWG Stage 1). Components declare
static buttonActivationBehaviors = true
and delegate to the controller for keyboard activation, focusability,
ARIA role, commandfor / command attribute handling,
and CommandEvent dispatch. See the README for what the shim
can and cannot polyfill relative to the forthcoming
HTMLSubmitButtonBehavior API.