Skip to content

Accessibility Semantics

This lesson explains Accessibility Semantics with clear examples, use cases, and best practices so you can build reusable Web Components confidently.

Web Components Accessibility Semantics Overview

Accessibility semantics help assistive technologies understand the purpose, structure, state, and behavior of custom elements. When building Web Components, developers must provide meaningful HTML, ARIA roles, labels, keyboard support, focus handling, and clear state updates.

Native HTML elements already include strong accessibility semantics. Custom elements do not automatically communicate meaning unless developers add the correct semantics and interaction patterns.

Web Components Accessibility Semantics List

  • 1. Prefer native HTML before ARIA.
    Use semantic HTML elements whenever possible because they include built-in roles, keyboard behavior, focus handling, and screen reader support.
    <button type="button">
      Save
    </button>
  • 2. Avoid clickable div elements.
    A <div> does not behave like a button by default. It does not automatically support keyboard activation, focus, or button semantics.
    <button type="button">
      Open Menu
    </button>
  • 3. Give custom elements an accessible name.
    Custom elements should expose a meaningful label using visible text, aria-label, aria-labelledby, or slotted text.
    <my-icon-button aria-label="Close dialog">
    </my-icon-button>
  • 4. Use roles only when needed.
    ARIA roles should supplement missing semantics, not replace correct HTML. Use roles carefully when a custom element represents a known widget pattern.
    <my-tabs role="tablist">
      <button role="tab" aria-selected="true">
        Overview
      </button>
    </my-tabs>
  • 5. Reflect component state with ARIA attributes.
    Interactive custom elements must communicate state such as expanded, selected, checked, pressed, disabled, hidden, or invalid.
    <button
      type="button"
      aria-expanded="false"
      aria-controls="menu-panel">
      Menu
    </button>
  • 6. Keep ARIA state synchronized with UI state.
    When a component opens, closes, selects, disables, or validates, update both the visual UI and the ARIA state.
    button.setAttribute(
      "aria-expanded",
      String(isOpen)
    );
  • 7. Use aria-controls to connect triggers and panels.
    When a button opens a panel, menu, dialog, or accordion content, aria-controls can identify the controlled element.
    <button
      type="button"
      aria-expanded="true"
      aria-controls="faq-1">
      What is Shadow DOM?
    </button>
    
    <div id="faq-1">
      Shadow DOM creates encapsulated DOM.
    </div>
  • 8. Use aria-labelledby for visible labels.
    If a visible heading or text labels a custom component, connect it with aria-labelledby.
    <h2 id="profile-title">
      Profile Settings
    </h2>
    
    <my-settings-panel
      aria-labelledby="profile-title">
    </my-settings-panel>
  • 9. Use aria-describedby for helper text.
    Form-related Web Components should connect inputs with helper text, validation text, or instructions.
    <my-text-field
      aria-describedby="email-help">
    </my-text-field>
    
    <p id="email-help">
      Enter your work email address.
    </p>
  • 10. Expose disabled state correctly.
    Native controls support disabled. Custom elements may also need aria-disabled and logic to prevent interaction.
    <my-button aria-disabled="true">
      Submit
    </my-button>
  • 11. Manage focus for interactive components.
    Components such as dialogs, menus, tabs, comboboxes, and accordions must provide predictable focus behavior.
    const firstButton =
      dialog.querySelector("button");
    
    firstButton.focus();
  • 12. Use delegatesFocus when appropriate.
    Shadow DOM can delegate focus to an internal focusable element when the host receives focus.
    this.attachShadow(
      {
        mode: "open",
        delegatesFocus: true
      }
    );
  • 13. Make the host focusable only when needed.
    Add tabindex="0" only when the custom element itself should be focusable. Avoid unnecessary tab stops.
    <my-card tabindex="0">
      Account summary
    </my-card>
  • 14. Support keyboard activation.
    Custom button-like components should support Enter and Space activation.
    host.addEventListener("keydown", (event) => {
      if (event.key === "Enter" || event.key === " ") {
        event.preventDefault();
        host.click();
      }
    });
  • 15. Preserve visible focus indicators.
    Never remove outlines without providing a clear replacement. Keyboard users need to see where focus is.
    :host(:focus-visible) {
      outline: 3px solid currentColor;
      outline-offset: 3px;
    }
  • 16. Use slots without losing semantics.
    Slotted content should keep meaningful heading, list, button, and form semantics.
    <my-card>
      <h2 slot="title">
        Billing
      </h2>
    
      <p>
        Manage invoices and payment methods.
      </p>
    </my-card>
  • 17. Do not rely on Shadow DOM alone for accessibility.
    Shadow DOM encapsulates implementation details, but accessibility still depends on names, roles, states, focus behavior, and keyboard support.
  • 18. Use live regions for dynamic updates.
    When status messages change without page reload, use an accessible live region.
    <div role="status" aria-live="polite">
      Saved successfully.
    </div>
  • 19. Announce validation errors clearly.
    Form components should expose invalid state and connect the field to the error message.
    <input
      aria-invalid="true"
      aria-describedby="email-error" />
    
    <p id="email-error">
      Email is required.
    </p>
  • 20. Use semantic headings inside component content.
    Components should not break the page heading structure. Use headings according to page hierarchy.
    <my-panel>
      <h2>Account Details</h2>
    </my-panel>
  • 21. Avoid hiding accessible content incorrectly.
    display: none, hidden, and aria-hidden="true" remove content from assistive technologies. Use them only when content should truly be hidden.
  • 22. Do not put focusable content inside aria-hidden.
    Focusable elements hidden from assistive technologies create confusing keyboard and screen reader behavior.
    <div aria-hidden="true">
      <button type="button">
        Hidden but focusable problem
      </button>
    </div>
  • 23. Support form participation when building custom fields.
    Form-associated custom elements can participate in forms using ElementInternals.
    class MyInput extends HTMLElement {
      static formAssociated = true;
    
      constructor() {
        super();
    
        this.internals =
          this.attachInternals();
      }
    
      set value(value) {
        this.internals.setFormValue(value);
      }
    }
  • 24. Use ElementInternals for accessible semantics.
    ElementInternals can help custom elements expose role, labels, form value, and validity behavior.
    this.internals =
      this.attachInternals();
    
    this.internals.role =
      "button";
  • 25. Set custom element roles carefully.
    Choose roles that match the component behavior. A role without matching keyboard behavior creates an accessibility mismatch.
    <my-toggle
      role="switch"
      aria-checked="false"
      tabindex="0">
      Notifications
    </my-toggle>
  • 26. Use correct semantics for tabs.
    Tabs require a tablist, tab controls, selected state, and connected tab panels.
    <div role="tablist">
      <button
        role="tab"
        aria-selected="true"
        aria-controls="panel-1">
        Details
      </button>
    </div>
    
    <section
      id="panel-1"
      role="tabpanel">
      Details content
    </section>
  • 27. Use correct semantics for accordions.
    Accordion headers should use buttons with aria-expanded and aria-controls.
    <h3>
      <button
        type="button"
        aria-expanded="false"
        aria-controls="section-1">
        Shipping
      </button>
    </h3>
  • 28. Use correct semantics for dialogs.
    Dialogs need a label, modal behavior when appropriate, focus management, and a clear close action.
    <div
      role="dialog"
      aria-modal="true"
      aria-labelledby="dialog-title">
      <h2 id="dialog-title">
        Confirm Delete
      </h2>
    </div>
  • 29. Use semantic list markup for repeated items.
    Menus, cards, options, and repeated content should use lists when the content is naturally a list.
    <ul>
      <li>Dashboard</li>
      <li>Reports</li>
      <li>Settings</li>
    </ul>
  • 30. Test Web Components with assistive technologies.
    Test keyboard navigation, focus order, screen reader names, roles, states, zoom, reduced motion, high contrast mode, and form behavior.

Accessible Custom Toggle Example

This example shows a custom element with role, state, keyboard support, and accessible naming.

class MyToggle extends HTMLElement {
  constructor() {
    super();

    this.checked =
      false;

    this.setAttribute("role", "switch");
    this.setAttribute("tabindex", "0");
    this.setAttribute("aria-checked", "false");

    this.addEventListener("click", () => {
      this.toggle();
    });

    this.addEventListener("keydown", (event) => {
      if (event.key === "Enter" || event.key === " ") {
        event.preventDefault();
        this.toggle();
      }
    });
  }

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

    this.setAttribute(
      "aria-checked",
      String(this.checked)
    );
  }
}

customElements.define(
  "my-toggle",
  MyToggle
);
<my-toggle>
  Email notifications
</my-toggle>

Common Web Components Accessibility Mistakes

  • Building custom buttons instead of using native <button>.
  • Adding ARIA roles without matching keyboard behavior.
  • Forgetting accessible names for icon-only components.
  • Failing to update aria-expanded, aria-selected, or aria-checked.
  • Removing focus outlines without replacement styles.
  • Creating unnecessary tab stops with tabindex="0".
  • Using positive tabindex values.
  • Hiding focusable content with aria-hidden="true".
  • Not testing slotted content with screen readers.
  • Assuming Shadow DOM automatically solves accessibility.

Accessibility Semantics Best Practices

  • Use native semantic HTML first.
  • Add ARIA only when native semantics are not enough.
  • Ensure every interactive component has an accessible name.
  • Keep visual state and ARIA state synchronized.
  • Provide full keyboard support for custom widgets.
  • Preserve visible focus indicators.
  • Use live regions for important dynamic updates.
  • Support labels, descriptions, validation, and form participation.
  • Test with keyboard, screen readers, zoom, and high contrast mode.
  • Document accessibility requirements for every reusable component.

Pro Tip

The best accessible Web Component is usually built from native HTML inside the component. Use ARIA to fill semantic gaps, but never use ARIA as a replacement for correct behavior, keyboard support, focus management, and meaningful content.