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.