Skip to content

Accessible Web Components — Complete Guide

DodaTech Updated 2026-06-28 2 min read

In this tutorial, you will learn about Accessible Web Components. We cover key concepts, practical examples, and best practices to help you master this topic.

Accessible Web Components require proper ARIA attributes, keyboard handling, focus management, and semantic structure for assistive technology compatibility.

What You'll Learn

  • How to add ARIA roles and attributes to custom elements
  • How to manage focus within components
  • How to handle keyboard navigation
  • How to test component Accessibility

Why It Matters

Custom elements have no built-in semantics. A <custom-button> is treated as a generic element by screen readers. You must provide the semantic meaning through ARIA.

flowchart LR
  A[Accessible Component] --> B[ARIA Attributes]
  A --> C[Keyboard Support]
  A --> D[Focus Management]
  A --> E[Semantic Structure]
  B --> F[role, aria-label, aria-expanded]
  C --> G[Enter, Space, Arrow, Escape]
  D --> H[tabindex, focus()]

ARIA for Custom Elements

class AccessibleButton extends HTMLElement {
    constructor() {
        super();
        this.attachShadow({ mode: 'open' });
        this.shadowRoot.innerHTML = `
            <style>
                .btn { padding: 8px 16px; background: #3498db; color: white; border: none; border-radius: 4px; cursor: pointer; }
                .btn:focus-visible { outline: 2px solid #2ecc71; outline-offset: 2px; }
            </style>
            <div class="btn" role="button" tabindex="0">
                <slot></slot>
            </div>
        `;
    }

    connectedCallback() {
        const btn = this.shadowRoot.querySelector('.btn');
        btn.addEventListener('keydown', (e) => {
            if (e.key === 'Enter' || e.key === ' ') {
                e.preventDefault();
                this.click();
            }
        });
        btn.addEventListener('click', () => {
            this.dispatchEvent(new CustomEvent('btn-click', { bubbles: true, composed: true }));
        });

        // Inherit ARIA from attributes
        if (this.hasAttribute('aria-label')) {
            btn.setAttribute('aria-label', this.getAttribute('aria-label'));
        }
        if (this.hasAttribute('disabled')) {
            btn.setAttribute('aria-disabled', 'true');
        }
    }

    get disabled() { return this.hasAttribute('disabled'); }
    set disabled(val) {
        if (val) {
            this.setAttribute('disabled', '');
            this.shadowRoot.querySelector('.btn').setAttribute('aria-disabled', 'true');
        } else {
            this.removeAttribute('disabled');
            this.shadowRoot.querySelector('.btn').removeAttribute('aria-disabled');
        }
    }
}
customElements.define('accessible-button', AccessibleButton);

Focus Management

class AccessibleDialog extends HTMLElement {
    constructor() {
        super();
        this.attachShadow({ mode: 'open' });
        this.shadowRoot.innerHTML = `
            <style>
                .overlay { display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.5); }
                .dialog { background: white; padding: 24px; border-radius: 8px; max-width: 500px; margin: 10vh auto; }
                :host([open]) .overlay { display: flex; }
            </style>
            <div class="overlay" role="dialog" aria-modal="true">
                <div class="dialog">
                    <slot></slot>
                </div>
            </div>
        `;
    }

    connectedCallback() {
        this.addEventListener('keydown', (e) => {
            if (e.key === 'Escape' && this.hasAttribute('open')) {
                this.close();
            }
        });
    }

    open() {
        this.setAttribute('open', '');
        this._previousFocus = document.activeElement;
        setTimeout(() => {
            const firstFocusable = this.shadowRoot.querySelector('button, [tabindex]:not([tabindex="-1"])');
            if (firstFocusable) firstFocusable.focus();
        }, 100);
    }

    close() {
        this.removeAttribute('open');
        if (this._previousFocus) {
            this._previousFocus.focus();
        }
    }
}
customElements.define('accessible-dialog', AccessibleDialog);

Common Mistakes

  1. Forgetting ARIA roles on custom interactive elements
  2. Not managing focus when showing/hiding content
  3. Ignoring keyboard navigation (Enter, Space, Escape, Arrows)
  4. Missing focus indicators (focus-visible styles)

Practice Questions

  1. What ARIA role should a custom button have? role="button".
  2. How do you make a div focusable? tabindex="0".
  3. What keyboard events must a custom button handle? Enter and Space.

Mini Project

Build an accessible dropdown menu component with proper ARIA (combobox, listbox, option roles), keyboard navigation (Arrow keys, Enter, Escape), and focus management. Test with a screen reader.

What's Next

Lesson 15: Component Communication

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro