Component Library a11y — Accessible Interactive Components
In this tutorial, you will learn about Component Library a11y. We cover key concepts, practical examples, and best practices to help you master this topic.
Accessible component library patterns use semantic HTML elements first, add ARIA only when necessary, ensure full keyboard operability, manage focus correctly, and provide visible states for hover focus and active.
What You'll Learn
You will learn how to build accessible components for a design system, how to choose between native HTML and ARIA, how to handle keyboard interactions, and how to manage focus in complex components.
Why It Matters
Components are the building blocks of every product built from the design system. If components are inaccessible, every product will be inaccessible. Accessible components ensure accessible products.
Real-World Use
DodaKit provides 25 accessible components. Each component uses native HTML elements where possible. ARIA is added only for custom interaction patterns like tabs and accordions. Every component includes keyboard support and focus management.
flowchart TD A[Component Patterns] --> B[Buttons] A --> C[Inputs] A --> D[Modals] A --> E[Accordions] A --> F[Tabs] A --> G[Menus] B --> B1[button element] C --> C1[label + input] D --> D1[Focus trap] E --> E1[aria-expanded] F --> F1[role tablist] G --> G1[keyboard navigation]
Native HTML First
Always start with native HTML elements. Native elements have built-in keyboard support, focus management, and screen reader announcements. Only use ARIA when the native element does not exist.
Keyboard Handlers
Every interactive component must be fully keyboard operable. Define keyboard interactions in the component documentation.
// Accessible accordion component pattern
class AccessibleAccordion {
constructor(sections) {
this.sections = sections.map((s, i) => ({
id: s.id || `accordion-section-${i}`,
title: s.title,
content: s.content,
expanded: s.expanded || false
}));
}
toggle(index) {
const section = this.sections[index];
if (!section) return;
section.expanded = !section.expanded;
return {
id: section.id,
expanded: section.expanded,
buttonId: `${section.id}-trigger`,
panelId: `${section.id}-panel`,
buttonLabel: `${section.title}, ${section.expanded ? 'expanded' : 'collapsed'}`
};
}
renderSectionHTML(section, index) {
return `<div class="ds-accordion__section">
<h3 class="ds-accordion__heading">
<button
id="${section.id}-trigger"
class="ds-accordion__trigger"
aria-expanded="${section.expanded}"
aria-controls="${section.id}-panel"
type="button">
${section.title}
<span class="ds-accordion__icon" aria-hidden="true">${section.expanded ? '-' : '+'}</span>
</button>
</h3>
<div
id="${section.id}-panel"
class="ds-accordion__panel"
role="region"
aria-labelledby="${section.id}-trigger"
${section.expanded ? '' : 'hidden'}>
<p>${section.content}</p>
</div>
</div>`;
}
getKeyboardInstructions() {
return {
'Tab': 'Move focus to next accordion trigger',
'Shift+Tab': 'Move focus to previous accordion trigger',
'Enter or Space': 'Toggle accordion section',
'Home': 'Move focus to first accordion trigger',
'End': 'Move focus to last accordion trigger'
};
}
}
const accordion = new AccessibleAccordion([
{ title: 'Scan settings', content: 'Configure your scan preferences including schedule and exclusions.' },
{ title: 'Notification preferences', content: 'Choose how you want to be notified about scan results.' }
]);
console.log(accordion.toggle(0));
console.log(accordion.renderSectionHTML(accordion.sections[0], 0));
Expected output:
{
id: 'accordion-section-0',
expanded: true,
buttonId: 'accordion-section-0-trigger',
panelId: 'accordion-section-0-panel',
buttonLabel: 'Scan settings, expanded'
}
<div class="ds-accordion__section">
<h3 class="ds-accordion__heading">
<button id="accordion-section-0-trigger" class="ds-accordion__trigger" aria-expanded="true" aria-controls="accordion-section-0-panel" type="button">
Scan settings
<span class="ds-accordion__icon" aria-hidden="true">-</span>
</button>
</h3>
<div id="accordion-section-0-panel" class="ds-accordion__panel" role="region" aria-labelledby="accordion-section-0-trigger">
<p>Configure your scan preferences including schedule and exclusions.</p>
</div>
</div>
Focus Management
For components that open additional UI (modals, dropdowns, menus), manage focus carefully. Trap focus inside modals. Return focus to the triggering element when the component closes.
<!-- Accessible modal component -->
<div class="ds-modal" role="dialog" aria-modal="true" aria-labelledby="modal-title">
<div class="ds-modal__backdrop" onclick="closeModal()"></div>
<div class="ds-modal__content" role="document">
<h2 id="modal-title" class="ds-modal__title">Confirm deletion</h2>
<p class="ds-modal__body">Are you sure you want to delete this scan result? This action cannot be undone.</p>
<div class="ds-modal__actions">
<button class="ds-button ds-button--secondary" type="button" onclick="closeModal()">Cancel</button>
<button class="ds-button ds-button--danger" type="button" onclick="confirmDelete()">Delete</button>
</div>
</div>
</div>
Common Mistakes
1. Using ARIA When Native HTML Works
ARIA is not needed for native button, input, select, or a elements. Using ARIA unnecessarily increases complexity and risk.
2. No Keyboard Support for Custom Widgets
Custom widgets like tabs, accordions, and menus must support arrow key navigation and standard keyboard interactions.
3. No Focus Trap in Modals
Modals that do not trap focus allow users to tab behind the modal, causing confusion and Accessibility failures.
4. No Visible Focus Indicator
Components that remove the default focus outline without providing an alternative fail SC 2.4.7.
5. Missing ARIA States
Components that expand or collapse need aria-expanded. Selected items need aria-selected or aria-current.
6. No Role for Custom Components
Custom components built from divs and spans need ARIA roles to convey their purpose to screen readers.
7. Inconsistent Component Behavior
The same component type should behave identically everywhere. If one button closes on Escape, all buttons should.
Practice Questions
1. What is the first rule of accessible components?
Use native HTML elements first. ARIA should only be added when no native element exists for the desired behavior.
2. What focus management is required for modals?
Focus must be trapped inside the modal when open. When closed, focus must return to the element that opened it.
3. What ARIA attribute indicates an expandable section?
aria-expanded with values true or false on the triggering button.
4. Why should components not remove the default focus outline?
SC 2.4.7 Focus Visible requires a visible focus indicator. If the default outline is removed, a custom indicator must replace it.
5. Challenge: Create an accessible tabs component with proper roles, keyboard navigation, and focus management.
FAQ
Mini Project
Create an accessible accordion component for a design system. Include proper ARIA attributes, keyboard support, and focus management. Write component documentation with keyboard interactions.
What's Next
Learn about Focus Indicators and how to design visible focus states for all interactive components. Then explore Accessible Forms System.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro