ARIA Without Anxiety: Making Component Libraries Accessible by Default
TABLE OF CONTENTS
- ARIA Is Powerful, but Easy to Misuse
- Common ARIA Pitfalls in Homegrown Components
- Designing Accessible Components in Your Library
- Testing and Validating Component Accessibility
- Maintaining Accessibility as Components Evolve
- Frequently Asked Questions
- Conclusion – Make Accessible the Default, Not the Exception
ARIA (Accessible Rich Internet Applications) intimidates many developers. The specification is vast. Misused ARIA can make accessibility worse. And the gap between "I read the docs" and "I implemented it correctly" is wider than it looks. This anxiety causes teams to either avoid ARIA entirely or use it incorrectly—both leading to inaccessible components.
But here's the good news: component libraries can encapsulate ARIA complexity, making accessible components the default rather than requiring every developer to become an ARIA expert. When your button component handles ARIA correctly, every team using that button gets accessibility for free.
This ARIA guide covers common pitfalls that trap developers, a pattern-driven approach to component design, and how to build and maintain accessible component libraries that make ARIA anxiety unnecessary.
ARIA Is Powerful, but Easy to Misuse
The First Rule of ARIA
What is ARIA and when should you use it? ARIA (Accessible Rich Internet Applications) adds semantics to HTML elements for assistive technologies. The first rule of ARIA: don't use ARIA when native HTML provides the semantics you need. Use ARIA only when building custom components that HTML elements don't cover.
The W3C's first rule of ARIA is clear: "If you can use a native HTML element with the semantics and behavior you require already built in, instead of repurposing an element and adding an ARIA role, state or property, then do so."
This rule exists because:
Native HTML works reliably: A <button> is universally understood by browsers and assistive technologies. An ARIA button requires more work to achieve equivalent functionality.
ARIA only adds semantics: ARIA tells assistive technologies what an element is or does—but doesn't add actual behavior. A <div role="button"> still requires keyboard handlers; a <button> has them built in.
Incorrect ARIA is worse than none: Misleading ARIA confuses assistive technology users. A visually hidden element marked aria-hidden="false" when it's actually aria-hidden="true" creates contradictory information.
Why Developers Struggle
ARIA difficulty stems from:
Specification complexity: ARIA includes dozens of roles, states, and properties with specific usage requirements. Learning it all is daunting.
Context dependency: The same ARIA attribute might be correct in one context and wrong in another. Evaluation requires understanding the full component structure.
Testing challenges: ARIA problems don't throw errors. You only discover issues through assistive technology testing—which many developers don't know how to do.
Evolving best practices: Recommendations change as browser and AT support evolves. What was best practice three years ago may not be now.
Framework abstractions: Modern frameworks add layers between developer intent and rendered output. Understanding how ARIA survives compilation requires framework-specific knowledge.
Common ARIA Pitfalls in Homegrown Components
Missing or Incorrect Roles
The role attribute tells assistive technologies what an element is. Common mistakes:
Dialogs without `role="dialog"`: Custom modal implementations using <div> without proper role. Screen readers don't recognize them as dialogs.
<!-- Wrong: no role, no accessibility -->
<div class="modal">
<div class="modal-content">...</div>
</div>
<!-- Right: proper role and labeling -->
<div role="dialog" aria-modal="true" aria-labelledby="dialog-title">
<h2 id="dialog-title">Confirm Action</h2>
<div class="modal-content">...</div>
</div>Tabs without `role="tablist"`, `role="tab"`, `role="tabpanel"`: Custom tab implementations that look like tabs but don't communicate tab semantics.
Menus without proper menu roles: Navigation that should be role="menu" with role="menuitem" children, or vice versa—navigation that shouldn't use menu roles at all.
Overriding implicit roles incorrectly: Adding roles to elements that already have the correct implicit role, or adding conflicting roles.
Misused ARIA Attributes
Even correct roles can be undermined by attribute errors:
`aria-hidden="true"` on visible content: Hides content from screen readers while it remains visible—creating invisible barriers.
Broken `aria-labelledby` references: IDs that don't exist or point to wrong elements.
<!-- Wrong: ID doesn't match -->
<input aria-labelledby="field-label">
<label id="fieldLabel">Name</label>
<!-- Right: IDs match -->
<input aria-labelledby="field-label">
<label id="field-label">Name</label>`aria-expanded` without corresponding content: Buttons marked as expandable without content that actually expands/collapses, or without updating the attribute on state change.
Static `aria-live` on dynamic containers: Using aria-live on containers that should announce changes but the attribute is applied incorrectly or the region isn't updating properly.
Focus Management Failures
ARIA communicates what elements are, but focus management determines user flow:
Modals without focus trapping: Dialog opens but focus stays on underlying page, or can Tab to elements behind the modal.
No focus movement on content changes: New content appears (toast, alert, dropdown) but focus doesn't move or announce the change.
Focus lost on element removal: Deleting an item removes the focused element without moving focus to a logical target.
Tab order misalignment: DOM order doesn't match visual order, creating confusing navigation.
Designing Accessible Components in Your Library
Pattern-Driven Approach
How do you build accessible components? Start with the WAI-ARIA Authoring Practices patterns, use semantic HTML as the foundation, add only necessary ARIA attributes, implement complete keyboard interaction, and document usage expectations for consumers.
Don't reinvent ARIA patterns. The W3C WAI-ARIA Authoring Practices Guide (APG) provides tested patterns for common components:
Available patterns include:
- Accordion
- Alert and Alert Dialog
- Breadcrumb
- Button (including toggle)
- Carousel
- Checkbox
- Combobox (autocomplete)
- Dialog (Modal)
- Disclosure
- Feed
- Grid
- Listbox
- Menu and Menubar
- Meter
- Radio Group
- Slider
- Spinbutton
- Switch
- Tabs
- Table
- Toolbar
- Tooltip
- Tree View
Each pattern documents:
- Required ARIA roles, states, and properties
- Required keyboard interactions
- Example implementations
- Additional guidance and considerations
Implementing the Button Pattern
Even "simple" components benefit from systematic implementation:
// Accessible button component
function Button({
children,
onClick,
disabled,
type = "button",
pressed, // for toggle buttons
expanded, // for buttons that control expandable content
controls, // ID of controlled element
describedby, // ID of description element
...props
}) {
return (
<button
type={type}
onClick={onClick}
disabled={disabled}
aria-pressed={pressed}
aria-expanded={expanded}
aria-controls={controls}
aria-describedby={describedby}
{...props}
>
{children}
</button>
);
}Key points:
- Uses native
<button>element (not<div role="button">) - Accepts ARIA attributes as props when needed
- Doesn't require consumers to remember ARIA
Implementing the Dialog Pattern
Dialogs require more ARIA and behavior:
// Accessible dialog component (simplified)
function Dialog({ isOpen, onClose, title, children }) {
const dialogRef = useRef();
const previousFocus = useRef();
useEffect(() => {
if (isOpen) {
// Store previous focus
previousFocus.current = document.activeElement;
// Move focus to dialog
dialogRef.current?.focus();
} else {
// Return focus
previousFocus.current?.focus();
}
}, [isOpen]);
useEffect(() => {
function handleKeyDown(e) {
if (e.key === "Escape" && isOpen) {
onClose();
}
}
document.addEventListener("keydown", handleKeyDown);
return () => document.removeEventListener("keydown", handleKeyDown);
}, [isOpen, onClose]);
if (!isOpen) return null;
return (
<div className="dialog-backdrop" onClick={onClose}>
<div
ref={dialogRef}
role="dialog"
aria-modal="true"
aria-labelledby="dialog-title"
tabIndex={-1}
onClick={e => e.stopPropagation()}
>
<h2 id="dialog-title">{title}</h2>
{children}
<button onClick={onClose}>Close</button>
</div>
</div>
);
}Key points:
role="dialog"andaria-modal="true"communicate modal naturearia-labelledbyconnects to title- Focus moves to dialog on open
- Focus returns to trigger on close
- Escape key closes dialog
- Full implementation would include focus trapping
Documented Expectations
Component documentation should include:
Usage guidelines:
- When to use this component
- When not to use it
- Required props and their effects
Keyboard interactions:
- What keys do what
- Expected focus behavior
Screen reader announcements:
- What users will hear
- How state changes are communicated
Do's and don'ts:
- Common mistakes to avoid
- Correct usage examples
Testing and Validating Component Accessibility
Unit Tests for Keyboard and ARIA
Automated tests verify component contracts:
describe('Dialog accessibility', () => {
it('has correct ARIA attributes', () => {
render(<Dialog isOpen title="Test" onClose={() => {}} />);
const dialog = screen.getByRole('dialog');
expect(dialog).toHaveAttribute('aria-modal', 'true');
expect(dialog).toHaveAttribute('aria-labelledby', 'dialog-title');
});
it('moves focus to dialog on open', () => {
render(<Dialog isOpen title="Test" onClose={() => {}} />);
expect(document.activeElement).toBe(screen.getByRole('dialog'));
});
it('closes on Escape key', () => {
const onClose = jest.fn();
render(<Dialog isOpen title="Test" onClose={onClose} />);
fireEvent.keyDown(document, { key: 'Escape' });
expect(onClose).toHaveBeenCalled();
});
it('traps focus within dialog', () => {
// Test that Tab cycles within dialog
// and doesn't escape to underlying page
});
});Use testing-library's accessibility queries (getByRole) to verify ARIA is working as expected.
Manual Testing with Screen Readers
Automated tests can't verify user experience. Manual testing is essential:
VoiceOver (Mac):
- Enable: Cmd + F5
- Navigate components with VoiceOver commands
- Verify announcements match expectations
NVDA (Windows):
- Download free from nvaccess.org
- Test with Firefox (best NVDA support)
- Verify component interaction and announcements
Screen reader testing checklist:
- [ ] Component role announced correctly
- [ ] State changes announced (expanded/collapsed, selected, etc.)
- [ ] Labels and descriptions announced
- [ ] Error messages announced when they appear
- [ ] Focus movement communicated
Storybook and Component Documentation
Use Storybook for accessibility validation:
Accessibility addon: Storybook's a11y addon runs automated checks on components in isolation.
Interactive testing: Storybook lets you interact with components in various states, facilitating manual testing.
Documentation: Storybook stories serve as living documentation of accessible usage.
// Storybook story with accessibility notes
export const PrimaryButton = () => <Button>Click me</Button>;
PrimaryButton.parameters = {
docs: {
description: {
story: 'Use for primary actions. Focusable, activates with Enter or Space.',
},
},
};Maintaining Accessibility as Components Evolve
Governance Around Component Changes
How do you prevent accessibility regressions in component libraries? Require accessibility review for all component changes, maintain comprehensive test coverage, run automated accessibility checks in CI, and document accessibility requirements in contribution guidelines.
Prevent accessibility regressions:
Change review process: All component library changes require accessibility review.
Accessibility checklist in PRs: PR templates include accessibility verification checklist.
Breaking change policies: Changes that affect ARIA or keyboard behavior require clear communication and migration guidance.
Version documentation: Changelog notes accessibility-relevant changes.
TestParty for Component Library Scanning
TestParty integrates with component development workflow:
Storybook scanning: Run TestParty against your Storybook to identify accessibility issues in components.
Pattern regression detection: Monitor component library documentation for accessibility changes between versions.
Issue tracking: When component accessibility issues are found in production, trace back to component library for fix.
CI integration: Prevent accessibility-breaking changes from merging.
Keeping Up with Standards
ARIA and accessibility best practices evolve:
Monitor APG updates: The WAI-ARIA Authoring Practices Guide is updated periodically with refined guidance.
Track browser/AT support: New ARIA features gain support over time. Previously unsupported features may become usable.
Engage accessibility community: Follow accessibility professionals who share updates and corrections to common practices.
Review and update: Periodically review component implementations against current best practices.
Frequently Asked Questions
When should I use ARIA vs. native HTML?
Use native HTML whenever it provides the semantics and behavior you need. Use ARIA only when creating custom components that don't have native HTML equivalents. For example, use <button> instead of <div role="button">, but use ARIA roles for tabs, dialogs, and other patterns without native HTML elements.
What's the most common ARIA mistake?
The most common mistake is adding ARIA without understanding its purpose—often adding redundant or conflicting information. For example, <button role="button"> is redundant; <a role="button"> may be incorrect depending on behavior. Close second: using aria-hidden="true" on visible, interactive content, hiding it from screen reader users.
How do I test if my ARIA is correct?
Combine approaches: automated testing catches structural issues (missing roles, broken ID references); manual testing with screen readers verifies user experience; unit tests verify keyboard behavior and ARIA state changes. No single approach is sufficient alone.
Should we build our own components or use an accessible library?
For common patterns, consider using established accessible component libraries like Radix, Headless UI, or React Aria. They've solved these problems already. Build custom when you have truly unique needs. Either way, verify accessibility through testing—no library is perfect.
How do we handle ARIA support differences across screen readers?
Test with multiple screen readers (VoiceOver, NVDA, JAWS) to understand variations. When support differs, prioritize approaches that work reasonably across all. Document known support limitations. The APG notes browser/AT support considerations for each pattern.
Conclusion – Make Accessible the Default, Not the Exception
ARIA without anxiety is possible when you build on solid foundations. Component libraries that encapsulate correct ARIA patterns let developers ship accessible interfaces without becoming ARIA experts themselves.
Building accessible components requires:
- Starting with native HTML and adding ARIA only when necessary
- Following established patterns from the WAI-ARIA Authoring Practices Guide
- Complete keyboard support implementing all expected interactions
- Comprehensive testing combining automated checks with manual AT testing
- Clear documentation so consumers use components correctly
- Governance processes preventing accessibility regressions
- Continuous maintenance keeping pace with evolving best practices
When accessibility is built into components, it becomes the default outcome of development rather than extra work. That's the goal: making accessible the path of least resistance.
Want a component-library-level view of your accessibility health? Book a demo and see how TestParty can scan and track component accessibility.
Related Articles:
Stay informed
Accessibility insights delivered
straight to your inbox.


Automate the software work for accessibility compliance, end-to-end.
Empowering businesses with seamless digital accessibility solutions—simple, inclusive, effective.
Book a Demo