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.