ElementInternals and Custom Element Accessibility

ElementInternals is a Web standard API that allows developers to manage internal behaviors of custom elements, such as form participation, custom validation, and accessibility semantics. By providing a bridge to the Accessibility Object Model (AOM), ElementInternals enables custom elements to expose semantic roles, states, and properties directly to assistive technologies without cluttering the host element’s HTML with manual ARIA attributes.

The Limitation of Traditional ARIA in Custom Elements

Historically, developers building Web Components had to set accessibility attributes directly on the custom element’s host tag using JavaScript:

class CustomToggle extends HTMLElement {
  connectedCallback() {
    this.setAttribute('role', 'switch');
    this.setAttribute('aria-checked', 'false');
  }
}

This approach creates several problems: 1. DOM Pollution: Internal implementation details leak into the public DOM. 2. Fragility: Consumers of the component can accidentally overwrite or remove attributes like role or aria-*, breaking accessibility. 3. Specificity & Defaults: There was no native mechanism to define “default” semantics that external consumers could safely override when necessary.

How ElementInternals Solves Accessibility

The ElementInternals interface exposes accessibility properties directly on an internal controller object created within the component’s constructor via this.attachInternals().

These ARIA properties map directly to the accessibility tree without reflecting as HTML attributes on the host element.

Key Features of Accessibility via ElementInternals

Implementation Example

Below is an implementation of an accessible toggle button using ElementInternals:

class AccessibleSwitch extends HTMLElement {
  #internals;
  #checked = false;

  constructor() {
    super();
    // 1. Attach the ElementInternals instance
    this.#internals = this.attachInternals();

    // 2. Set default accessibility semantics
    this.#internals.role = 'switch';
    this.#internals.ariaChecked = 'false';

    this.addEventListener('click', this.#toggle.bind(this));
    this.addEventListener('keydown', this.#handleKey.bind(this));
  }

  connectedCallback() {
    // Ensure the element is focusable if not disabled
    if (!this.hasAttribute('tabindex')) {
      this.tabIndex = 0;
    }
  }

  #toggle() {
    this.checked = !this.checked;
  }

  #handleKey(event) {
    if (event.key === ' ' || event.key === 'Enter') {
      event.preventDefault();
      this.#toggle();
    }
  }

  get checked() {
    return this.#checked;
  }

  set checked(value) {
    this.#checked = Boolean(value);
    // 3. Update the accessibility state dynamically
    this.#internals.ariaChecked = this.#checked ? 'true' : 'false';
  }
}

customElements.define('accessible-switch', AccessibleSwitch);

Common ARIA Properties Supported by ElementInternals

ElementInternals supports camelCase versions of standard ARIA attributes:

Using ElementInternals provides a robust, standardized mechanism for creating custom elements that behave like native browser elements in the accessibility tree.