--- name: ux-accessibility description: WCAG 2.2 accessibility patterns for web components. Use when implementing focus management, keyboard navigation, screen reader support, reduced motion, high contrast mode, or touch targets. Integrates with project's accessibility.css tokens. (project) allowed-tools: - Read - Write - Edit - Glob - Grep --- # UX Accessibility Skill Accessibility-first design patterns for WCAG 2.2 AA compliance. This skill provides implementation guidance for making interactive components accessible to all users. ## Related Skills - **`material-symbols-v3`**: Icon accessibility patterns (`aria-hidden`, `aria-label`) - **`ux-iconography`**: Icon button patterns and screen reader text - **`ux-component-states`**: ARIA state attributes for interactive elements ## Project Accessibility Tokens This project defines accessibility tokens in `css/styles/accessibility.css`: ```css :root { --focus-ring-width: 2px; --focus-ring-offset: 2px; --focus-ring-color: var(--color-primary); --min-touch-target: 44px; --transition-fast: 150ms ease; } ``` ## Focus Management ### Focus Ring Implementation Always use the project's focus tokens for consistent visible focus: ```css .interactive:focus-visible { outline: var(--focus-ring-width) solid var(--focus-ring-color); outline-offset: var(--focus-ring-offset); } ``` ### Focus Trap for Modals When implementing modals or dialogs, trap focus using direct element references (NO querySelector): ```javascript class ModalDialog extends HTMLElement { // Store focusable elements as direct references during construction #closeBtn; #confirmBtn; constructor() { super(); this.attachShadow({ mode: 'open' }); // Build DOM imperatively, storing references this.#closeBtn = document.createElement('button'); this.#closeBtn.className = 'close-btn'; this.#closeBtn.textContent = '×'; this.#closeBtn.setAttribute('part', 'close'); this.#confirmBtn = document.createElement('button'); this.#confirmBtn.className = 'confirm-btn'; this.#confirmBtn.textContent = 'Confirm'; this.#confirmBtn.setAttribute('part', 'confirm'); const container = document.createElement('div'); container.className = 'modal'; container.setAttribute('part', 'container'); container.appendChild(this.#closeBtn); container.appendChild(document.createElement('slot')); container.appendChild(this.#confirmBtn); this.shadowRoot.appendChild(container); } connectedCallback() { this.addEventListener('keydown', this); } disconnectedCallback() { this.removeEventListener('keydown', this); } handleEvent(e) { if (e.type === 'keydown' && e.key === 'Tab') { const active = this.shadowRoot.activeElement; if (e.shiftKey && active === this.#closeBtn) { e.preventDefault(); this.#confirmBtn.focus(); } else if (!e.shiftKey && active === this.#confirmBtn) { e.preventDefault(); this.#closeBtn.focus(); } } } } ``` ### Return Focus After Close Store and restore focus when closing overlays: ```javascript #previouslyFocused = null; open() { this.#previouslyFocused = document.activeElement; this.showModal(); } close() { this.close(); this.#previouslyFocused?.focus(); } ``` ## Keyboard Navigation ### Standard Patterns | Component | Key | Action | |-----------|-----|--------| | Button | Enter, Space | Activate | | Menu | Arrow keys | Navigate items | | Dialog | Escape | Close | | Tabs | Arrow keys | Switch tabs | | Listbox | Arrow keys, Home, End | Navigate options | ### Implementation Example (Web Component) ```javascript class KeyboardNav extends HTMLElement { // Direct element references - NO querySelector #items = []; #currentIndex = 0; constructor() { super(); this.attachShadow({ mode: 'open' }); // Build items during construction, store direct references } connectedCallback() { this.setAttribute('role', 'menu'); this.setAttribute('tabindex', '0'); this.addEventListener('keydown', this); } disconnectedCallback() { this.removeEventListener('keydown', this); } handleEvent(e) { if (e.type === 'keydown') { switch (e.key) { case 'Enter': case ' ': e.preventDefault(); this.#activate(); break; case 'Escape': this.#close(); break; case 'ArrowDown': e.preventDefault(); this.#focusNext(); break; case 'ArrowUp': e.preventDefault(); this.#focusPrevious(); break; } } } #focusNext() { this.#currentIndex = Math.min(this.#currentIndex + 1, this.#items.length - 1); this.#items[this.#currentIndex]?.focus(); } #focusPrevious() { this.#currentIndex = Math.max(this.#currentIndex - 1, 0); this.#items[this.#currentIndex]?.focus(); } #activate() { this.dispatchEvent(new CustomEvent('item-activated', { bubbles: true, composed: true, detail: { index: this.#currentIndex } })); } #close() { this.dispatchEvent(new CustomEvent('menu-close', { bubbles: true, composed: true })); } } ``` ## Screen Reader Support ### ARIA Attributes Required ARIA for interactive components: ```html