- Create comprehensive html-template skill with 3 reference files - E2E Testing Attributes (data-what, data-which patterns) - ARIA Accessibility Attributes (roles, properties, states, WCAG) - Combined Patterns (real-world examples with both) - Move E2E attribute guidance from command to skill - Add extensive ARIA accessibility documentation - Update angular-template skill with cross-references - Remove dev-add-e2e-attrs command (functionality now in skill) The new skill provides 3,322 lines of comprehensive documentation covering both testing and accessibility best practices for HTML templates, with practical examples for forms, navigation, tables, dialogs, and more. Benefits: - Clean separation: Angular syntax vs HTML attributes - Reusable: html-template works with any HTML context - Comprehensive: E2E + ARIA in one place - Integrated: Works seamlessly with angular-template skill
24 KiB
ARIA Accessibility Attributes - Complete Reference
This reference provides comprehensive guidance for implementing ARIA (Accessible Rich Internet Applications) attributes to make web applications accessible to all users, including those using assistive technologies.
Table of Contents
- Overview
- Why ARIA?
- Semantic HTML First
- ARIA Roles
- ARIA Properties
- ARIA States
- Live Regions
- Relationships
- Keyboard Navigation
- Patterns by Component Type
- Best Practices
- Testing
- WCAG Compliance
Overview
ARIA attributes enhance HTML with semantic information that assistive technologies (screen readers, voice control, etc.) use to provide better user experiences for people with disabilities.
Three categories of ARIA attributes:
- Roles: Define what an element is or does
- Properties: Provide additional context and relationships
- States: Indicate current state or dynamic changes
Why ARIA?
Legal Requirements
- ADA Compliance: Required for US organizations
- WCAG 2.1: International accessibility standard (A, AA, AAA levels)
- Section 508: US federal government requirement
- European Accessibility Act: EU requirement
User Benefits
- Screen reader users: Understand page structure and interactive elements
- Keyboard-only users: Navigate and interact effectively
- Voice control users: Command elements by name/role
- Users with cognitive disabilities: Clearer context and relationships
Business Benefits
- Reach wider audience (15% of population has disabilities)
- Improved SEO (accessibility correlates with search ranking)
- Better usability for all users
- Legal protection and compliance
- Enhanced brand reputation
Semantic HTML First
Always use semantic HTML before adding ARIA roles:
<!-- GOOD: Use native button -->
<button>Click Me</button>
<!-- BAD: Don't use div with role when native element exists -->
<div role="button" tabindex="0">Click Me</div>
<!-- GOOD: Use native navigation -->
<nav>
<a href="/home">Home</a>
</nav>
<!-- BAD: Don't add unnecessary role to semantic element -->
<nav role="navigation">
<a href="/home">Home</a>
</nav>
First rule of ARIA: If you can use a native HTML element or attribute with the semantics and behavior you require already built in, then do so. Only use ARIA when no native alternative exists.
ARIA Roles
Widget Roles (Interactive Components)
Button
<!-- Native button (preferred) -->
<button>Click Me</button>
<!-- Custom button (when necessary) -->
<div
role="button"
tabindex="0"
(click)="handleClick()"
(keydown.enter)="handleClick()"
(keydown.space)="handleClick()">
Click Me
</div>
Link
<!-- Native link (preferred) -->
<a href="/page">Go to Page</a>
<!-- Custom link role (rarely needed) -->
<span
role="link"
tabindex="0"
(click)="navigate()">
Go to Page
</span>
Checkbox
<!-- Native checkbox (preferred) -->
<input
type="checkbox"
[(ngModel)]="isChecked"
aria-label="Accept terms" />
<!-- Custom checkbox -->
<div
role="checkbox"
[attr.aria-checked]="isChecked"
tabindex="0"
(click)="toggle()">
Accept terms
</div>
Radio
<!-- Native radio (preferred) -->
<input
type="radio"
name="payment"
value="credit"
aria-label="Credit card payment" />
<!-- Custom radio group -->
<div role="radiogroup" aria-labelledby="payment-label">
<span id="payment-label">Payment Method</span>
<div
role="radio"
[attr.aria-checked]="selected === 'credit'"
tabindex="0">
Credit Card
</div>
<div
role="radio"
[attr.aria-checked]="selected === 'paypal'"
tabindex="-1">
PayPal
</div>
</div>
Textbox
<!-- Native input (preferred) -->
<input
type="text"
aria-label="Username"
aria-required="true" />
<!-- Custom textbox (contenteditable) -->
<div
role="textbox"
contenteditable="true"
aria-label="Rich text editor"
aria-multiline="true">
</div>
Menu and Menuitem
<div role="menu" aria-labelledby="menu-button">
<div role="menuitem" tabindex="0">
New File
</div>
<div role="menuitem" tabindex="-1">
Open File
</div>
<div role="separator"></div>
<div role="menuitem" tabindex="-1">
Exit
</div>
</div>
Tab, Tablist, Tabpanel
<div class="tabs">
<div role="tablist" aria-label="Content sections">
<button
role="tab"
[attr.aria-selected]="activeTab === 'profile'"
[attr.aria-controls]="'profile-panel'"
[attr.tabindex]="activeTab === 'profile' ? 0 : -1">
Profile
</button>
<button
role="tab"
[attr.aria-selected]="activeTab === 'settings'"
[attr.aria-controls]="'settings-panel'"
[attr.tabindex]="activeTab === 'settings' ? 0 : -1">
Settings
</button>
</div>
<div
role="tabpanel"
id="profile-panel"
[attr.aria-hidden]="activeTab !== 'profile'"
aria-labelledby="profile-tab">
<!-- Profile content -->
</div>
<div
role="tabpanel"
id="settings-panel"
[attr.aria-hidden]="activeTab !== 'settings'"
aria-labelledby="settings-tab">
<!-- Settings content -->
</div>
</div>
Landmark Roles (Page Structure)
Banner (Header)
<!-- Implicit landmark (preferred) -->
<header>
<h1>Site Title</h1>
</header>
<!-- Explicit role (when header isn't appropriate) -->
<div role="banner">
<h1>Site Title</h1>
</div>
Navigation
<!-- Implicit landmark (preferred) -->
<nav aria-label="Main navigation">
<a href="/">Home</a>
<a href="/about">About</a>
</nav>
<!-- Multiple navigation areas need labels -->
<nav aria-label="Primary navigation">
<!-- Main menu -->
</nav>
<nav aria-label="Footer navigation">
<!-- Footer menu -->
</nav>
Main
<!-- Implicit landmark (preferred) -->
<main>
<!-- Main content -->
</main>
<!-- Only one main landmark per page -->
Complementary (Aside)
<aside aria-label="Related articles">
<!-- Sidebar content -->
</aside>
Contentinfo (Footer)
<footer>
<p>© 2024 Company Name</p>
</footer>
Search
<form role="search" aria-label="Site search">
<input type="search" aria-label="Search query" />
<button type="submit">Search</button>
</form>
Region
<!-- Generic landmark with label -->
<section aria-labelledby="news-heading">
<h2 id="news-heading">Latest News</h2>
<!-- Content -->
</section>
Document Structure Roles
Article
<article aria-labelledby="article-title">
<h2 id="article-title">Article Title</h2>
<!-- Article content -->
</article>
List and Listitem
<!-- Native list (preferred) -->
<ul>
<li>Item 1</li>
<li>Item 2</li>
</ul>
<!-- Custom list (when necessary) -->
<div role="list" aria-label="Product list">
<div role="listitem">Product 1</div>
<div role="listitem">Product 2</div>
</div>
Dialog Roles
Dialog
<div
role="dialog"
aria-labelledby="dialog-title"
aria-describedby="dialog-description"
aria-modal="true">
<h2 id="dialog-title">Confirm Action</h2>
<p id="dialog-description">Are you sure you want to proceed?</p>
<button>Confirm</button>
<button>Cancel</button>
</div>
Alertdialog
<div
role="alertdialog"
aria-labelledby="alert-title"
aria-describedby="alert-description"
aria-modal="true">
<h2 id="alert-title">Error</h2>
<p id="alert-description">An error occurred while saving.</p>
<button>OK</button>
</div>
ARIA Properties
aria-label
Provides accessible name when no visible label exists:
<!-- Icon button without visible text -->
<button aria-label="Close dialog">
<i class="icon-close"></i>
</button>
<!-- Search input -->
<input
type="search"
aria-label="Search products"
placeholder="Search..." />
aria-labelledby
References existing element(s) as label:
<!-- Form field with visible label -->
<label id="email-label">Email Address</label>
<input
type="email"
aria-labelledby="email-label" />
<!-- Dialog with title -->
<div
role="dialog"
aria-labelledby="dialog-title">
<h2 id="dialog-title">Settings</h2>
<!-- Dialog content -->
</div>
<!-- Multiple labels (concatenated) -->
<div
role="button"
aria-labelledby="btn-icon btn-text">
<span id="btn-icon">💾</span>
<span id="btn-text">Save</span>
</div>
aria-describedby
Provides additional description:
<!-- Input with help text -->
<input
type="password"
aria-label="Password"
aria-describedby="password-hint" />
<span id="password-hint">
Must be at least 8 characters
</span>
<!-- Button with additional context -->
<button
aria-label="Delete item"
aria-describedby="delete-warning">
Delete
</button>
<span id="delete-warning">
This action cannot be undone
</span>
aria-required
Indicates required form fields:
<input
type="email"
aria-label="Email address"
aria-required="true" />
<!-- Or use native HTML5 required -->
<input
type="email"
aria-label="Email address"
required />
aria-placeholder
Describes expected value (use sparingly):
<input
type="text"
aria-label="Username"
aria-placeholder="john_doe123" />
<!-- Native placeholder is usually sufficient -->
<input
type="text"
aria-label="Username"
placeholder="john_doe123" />
aria-invalid
Indicates validation errors:
<input
type="email"
aria-label="Email"
[attr.aria-invalid]="hasError ? 'true' : null"
aria-describedby="email-error" />
@if (hasError) {
<span id="email-error" role="alert">
Please enter a valid email address
</span>
}
aria-haspopup
Indicates element triggers popup:
<!-- Menu button -->
<button
aria-label="Actions menu"
aria-haspopup="menu"
[attr.aria-expanded]="isMenuOpen">
Actions ▼
</button>
<!-- Dialog trigger -->
<button
aria-label="Open settings"
aria-haspopup="dialog">
Settings
</button>
ARIA States
aria-expanded
Indicates expand/collapse state:
<button
(click)="toggleAccordion()"
[attr.aria-expanded]="isExpanded"
aria-controls="panel-content">
Toggle Panel
</button>
<div
id="panel-content"
[attr.aria-hidden]="!isExpanded">
<!-- Panel content -->
</div>
aria-selected
Indicates selection in lists or tabs:
@for (tab of tabs; track tab.id) {
<button
role="tab"
[attr.aria-selected]="activeTab === tab.id"
[attr.tabindex]="activeTab === tab.id ? 0 : -1">
{{ tab.label }}
</button>
}
aria-checked
Indicates checkbox/radio state:
<!-- Native checkbox handles this automatically -->
<input
type="checkbox"
[(ngModel)]="isChecked" />
<!-- Custom checkbox needs explicit aria-checked -->
<div
role="checkbox"
[attr.aria-checked]="isChecked"
tabindex="0">
{{ label }}
</div>
aria-disabled
Indicates disabled state:
<button
[disabled]="!isValid"
[attr.aria-disabled]="!isValid">
Submit
</button>
<!-- For elements that don't support disabled attribute -->
<div
role="button"
[attr.aria-disabled]="!canPerformAction"
[class.disabled]="!canPerformAction">
Action
</div>
aria-hidden
Hides element from assistive technology:
<!-- Hide decorative icons -->
<button>
<i class="icon-save" aria-hidden="true"></i>
Save
</button>
<!-- Hide collapsed content -->
<div [attr.aria-hidden]="!isVisible">
<!-- Content -->
</div>
aria-pressed
Indicates toggle button state:
<button
[attr.aria-pressed]="isActive"
(click)="toggle()">
{{ isActive ? 'Active' : 'Inactive' }}
</button>
aria-current
Indicates current item in set:
<!-- Current page in navigation -->
<nav>
<a href="/home" [attr.aria-current]="currentPage === 'home' ? 'page' : null">
Home
</a>
<a href="/about" [attr.aria-current]="currentPage === 'about' ? 'page' : null">
About
</a>
</nav>
<!-- Current step in process -->
<ol>
@for (step of steps; track step.id) {
<li [attr.aria-current]="currentStep === step.id ? 'step' : null">
{{ step.label }}
</li>
}
</ol>
Live Regions
Live regions announce dynamic content changes to screen readers:
aria-live
<!-- Polite: Waits for pause in speech -->
<div aria-live="polite" aria-atomic="true">
{{ statusMessage }}
</div>
<!-- Assertive: Interrupts immediately -->
<div aria-live="assertive" aria-atomic="true">
{{ criticalError }}
</div>
role="alert"
Implicit aria-live="assertive":
<div role="alert">
{{ errorMessage }}
</div>
role="status"
Implicit aria-live="polite":
<div role="status">
{{ successMessage }}
</div>
aria-atomic
Controls whether entire region or just changes are announced:
<!-- Announce entire region content -->
<div aria-live="polite" aria-atomic="true">
Loading: {{ progress }}%
</div>
<!-- Only announce what changed -->
<div aria-live="polite" aria-atomic="false">
Items: {{ itemCount }}
</div>
aria-relevant
Controls what changes trigger announcements:
<div
aria-live="polite"
aria-relevant="additions removals text">
<!-- Announcements for additions, removals, and text changes -->
</div>
Relationships
aria-controls
Identifies controlled elements:
<button
(click)="togglePanel()"
aria-controls="panel-content"
[attr.aria-expanded]="isPanelOpen">
Toggle
</button>
<div id="panel-content">
<!-- Panel content -->
</div>
aria-owns
Defines parent-child relationship when DOM structure doesn't:
<div role="tree" aria-owns="child1 child2">
<!-- Tree root -->
</div>
<!-- Children elsewhere in DOM -->
<div id="child1" role="treeitem">Child 1</div>
<div id="child2" role="treeitem">Child 2</div>
aria-activedescendant
Indicates active descendant in composite widget:
<div
role="listbox"
[attr.aria-activedescendant]="'option-' + activeIndex"
tabindex="0">
@for (option of options; track option.id; let idx = $index) {
<div
role="option"
[id]="'option-' + idx"
[class.active]="idx === activeIndex">
{{ option.label }}
</div>
}
</div>
aria-flowto
Defines reading order when visual order differs:
<div id="section1" aria-flowto="section3">
Section 1
</div>
<div id="section2">
Section 2 (sidebar)
</div>
<div id="section3">
Section 3 (continues from section 1)
</div>
Keyboard Navigation
Proper keyboard navigation is essential for accessibility:
Tabindex
<!-- tabindex="0": Include in tab order (natural position) -->
<div role="button" tabindex="0">Clickable</div>
<!-- tabindex="-1": Focusable programmatically, not in tab order -->
<div role="menuitem" tabindex="-1">Menu Item</div>
<!-- tabindex="1+": Custom tab order (avoid unless necessary) -->
<input tabindex="1" />
<input tabindex="2" />
Keyboard Event Handlers
<div
role="button"
tabindex="0"
(click)="handleAction()"
(keydown.enter)="handleAction()"
(keydown.space)="handleAction()">
Custom Button
</div>
<!-- Arrow key navigation -->
<div
role="listbox"
tabindex="0"
(keydown.arrowDown)="moveNext()"
(keydown.arrowUp)="movePrevious()"
(keydown.home)="moveFirst()"
(keydown.end)="moveLast()">
<!-- List items -->
</div>
Focus Management
// Move focus programmatically
@ViewChild('dialogElement') dialogElement!: ElementRef;
openDialog() {
this.showDialog = true;
setTimeout(() => {
this.dialogElement.nativeElement.focus();
});
}
Patterns by Component Type
Form with Validation
<form>
<div class="field">
<label id="username-label" for="username">Username</label>
<input
id="username"
type="text"
[(ngModel)]="username"
aria-labelledby="username-label"
aria-required="true"
[attr.aria-invalid]="usernameError ? 'true' : null"
aria-describedby="username-hint username-error" />
<span id="username-hint">3-20 characters</span>
@if (usernameError) {
<span id="username-error" role="alert">
{{ usernameError }}
</span>
}
</div>
</form>
Modal Dialog
<div
*ngIf="isOpen"
class="modal-overlay"
(click)="closeDialog()">
<div
class="modal"
role="dialog"
aria-modal="true"
aria-labelledby="modal-title"
aria-describedby="modal-description"
(click)="$event.stopPropagation()"
#modalElement
tabindex="-1">
<h2 id="modal-title">Confirm Delete</h2>
<p id="modal-description">
Are you sure you want to delete this item?
</p>
<button
(click)="confirm()"
aria-label="Confirm deletion">
Delete
</button>
<button
(click)="cancel()"
aria-label="Cancel deletion">
Cancel
</button>
</div>
</div>
Accordion
@for (section of sections; track section.id) {
<div class="accordion-item">
<button
(click)="toggle(section.id)"
[attr.aria-expanded]="section.expanded"
aria-controls="panel-{{ section.id }}">
{{ section.title }}
</button>
<div
id="panel-{{ section.id }}"
[attr.aria-hidden]="!section.expanded"
role="region"
[attr.aria-labelledby]="'heading-' + section.id">
{{ section.content }}
</div>
</div>
}
Accessible Data Table
<table role="table" aria-labelledby="table-title">
<caption id="table-title">Customer Orders</caption>
<thead>
<tr>
<th scope="col">Order ID</th>
<th scope="col">Customer</th>
<th scope="col">Total</th>
<th scope="col">Actions</th>
</tr>
</thead>
<tbody>
@for (order of orders; track order.id) {
<tr>
<td>{{ order.id }}</td>
<td>{{ order.customer }}</td>
<td>{{ order.total | currency }}</td>
<td>
<button [attr.aria-label]="'View order ' + order.id">
View
</button>
</td>
</tr>
}
</tbody>
</table>
Toast Notifications
<div
class="toast"
role="status"
aria-live="polite"
aria-atomic="true"
*ngIf="showToast">
{{ toastMessage }}
</div>
<!-- For errors, use role="alert" -->
<div
class="toast error"
role="alert"
aria-live="assertive"
aria-atomic="true"
*ngIf="showError">
{{ errorMessage }}
</div>
Loading Indicator
<div
class="loading"
role="status"
aria-live="polite"
aria-label="Loading content">
<span aria-hidden="true">⏳</span>
<span>Loading...</span>
</div>
Best Practices
Do's ✅
-
Use semantic HTML first
- Prefer
<button>over<div role="button"> - Use native form controls when possible
- Leverage built-in accessibility of HTML elements
- Prefer
-
Provide text alternatives
- All images need
alttext - Icon-only buttons need
aria-label - Complex controls need
aria-describedby
- All images need
-
Maintain keyboard accessibility
- All interactive elements must be keyboard accessible
- Implement proper focus management
- Use
tabindexappropriately
-
Keep states in sync
- Visual state must match ARIA state
- Update
aria-expanded,aria-selected, etc. when changing UI
-
Test with assistive technology
- Screen readers (NVDA, JAWS, VoiceOver)
- Keyboard-only navigation
- Voice control software
-
Announce dynamic changes
- Use live regions for status updates
- Announce errors and success messages
- Alert users to important changes
Don'ts ❌
-
Don't override native semantics unnecessarily
- ❌
<button role="link">(use<a>instead) - ❌
<h1 role="button">(use<button>or style differently)
- ❌
-
Don't use ARIA to fix bad HTML
- Fix the HTML structure instead
- Use semantic elements properly
-
Don't hide focusable elements with aria-hidden
- ❌
<button aria-hidden="true">(removes from all users) - Use
visibility: hiddenordisplay: noneinstead
- ❌
-
Don't create keyboard traps
- Ensure users can navigate out of components
- Implement proper focus management in modals
-
Don't forget mobile accessibility
- Touch targets should be at least 44x44 pixels
- Ensure gestures have keyboard equivalents
- Test with mobile screen readers
Testing
Automated Testing Tools
- axe DevTools: Browser extension for accessibility audits
- WAVE: Web accessibility evaluation tool
- Lighthouse: Built into Chrome DevTools
- Pa11y: Command-line accessibility testing
Manual Testing
-
Keyboard navigation
- Tab through all interactive elements
- Use arrow keys where appropriate
- Test Escape, Enter, and Space keys
-
Screen reader testing
- Windows: NVDA (free), JAWS (commercial)
- macOS: VoiceOver (built-in)
- Mobile: TalkBack (Android), VoiceOver (iOS)
-
Visual testing
- Zoom to 200%
- Test high contrast mode
- Check color contrast ratios
Unit Test Example
describe('Accessibility', () => {
it('should have proper ARIA attributes', () => {
const button = fixture.nativeElement.querySelector('[data-what="submit-button"]');
expect(button.getAttribute('aria-label')).toBe('Submit form');
expect(button.getAttribute('role')).toBeFalsy(); // Native button, no role needed
});
it('should toggle aria-expanded', () => {
const toggle = fixture.nativeElement.querySelector('[data-what="toggle-button"]');
expect(toggle.getAttribute('aria-expanded')).toBe('false');
toggle.click();
fixture.detectChanges();
expect(toggle.getAttribute('aria-expanded')).toBe('true');
});
});
WCAG Compliance
WCAG 2.1 Levels
- Level A: Minimum accessibility (legal requirement in many jurisdictions)
- Level AA: Commonly targeted level (includes color contrast, text sizing)
- Level AAA: Highest level (may not be achievable for all content)
Key WCAG Requirements
Perceivable
- Provide text alternatives for non-text content
- Ensure sufficient color contrast (4.5:1 for normal text, 3:1 for large text)
- Make content available to assistive technologies
Operable
- Make all functionality keyboard accessible
- Provide enough time for users to read and interact
- Don't use content that causes seizures (flashing)
- Help users navigate and find content
Understandable
- Make text readable and understandable
- Make content appear and operate in predictable ways
- Help users avoid and correct mistakes
Robust
- Maximize compatibility with assistive technologies
- Use valid, well-formed HTML
- Ensure ARIA is used correctly
Color Contrast Checker
<!-- Minimum contrast ratios (WCAG AA) -->
<!-- Normal text (< 18pt): 4.5:1 -->
<!-- Large text (≥ 18pt or ≥ 14pt bold): 3:1 -->
<!-- UI components and graphics: 3:1 -->
<button class="primary">
<!-- Ensure button text meets contrast requirements -->
Click Me
</button>
Related Documentation
- E2E Testing Attributes - E2E testing guidance
- Combined Patterns - Examples with E2E + ARIA together
- WCAG 2.1 Guidelines - Official WCAG quick reference
- ARIA Authoring Practices Guide - Official ARIA patterns
Summary
ARIA attributes are essential for:
- ✅ Making applications accessible to all users
- ✅ Meeting legal requirements (ADA, WCAG, Section 508)
- ✅ Providing excellent user experience with assistive technology
- ✅ Improving SEO and usability for everyone
- ✅ Demonstrating social responsibility and inclusivity
Always use semantic HTML first, then enhance with ARIA when needed.