Files
ISA-Frontend/.claude/skills/html-template/references/aria-attributes.md
Lorenz Hilpert bf30ec1213 feat(skills): create html-template skill for E2E and ARIA attributes
- 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
2025-10-29 13:21:29 +01:00

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

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:

  1. Roles: Define what an element is or does
  2. Properties: Provide additional context and relationships
  3. States: Indicate current state or dynamic changes

Why ARIA?

  • 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>
<!-- 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>
<footer>
  <p>&copy; 2024 Company Name</p>
</footer>
<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

  1. Use semantic HTML first

    • Prefer <button> over <div role="button">
    • Use native form controls when possible
    • Leverage built-in accessibility of HTML elements
  2. Provide text alternatives

    • All images need alt text
    • Icon-only buttons need aria-label
    • Complex controls need aria-describedby
  3. Maintain keyboard accessibility

    • All interactive elements must be keyboard accessible
    • Implement proper focus management
    • Use tabindex appropriately
  4. Keep states in sync

    • Visual state must match ARIA state
    • Update aria-expanded, aria-selected, etc. when changing UI
  5. Test with assistive technology

    • Screen readers (NVDA, JAWS, VoiceOver)
    • Keyboard-only navigation
    • Voice control software
  6. Announce dynamic changes

    • Use live regions for status updates
    • Announce errors and success messages
    • Alert users to important changes

Don'ts

  1. Don't override native semantics unnecessarily

    • <button role="link"> (use <a> instead)
    • <h1 role="button"> (use <button> or style differently)
  2. Don't use ARIA to fix bad HTML

    • Fix the HTML structure instead
    • Use semantic elements properly
  3. Don't hide focusable elements with aria-hidden

    • <button aria-hidden="true"> (removes from all users)
    • Use visibility: hidden or display: none instead
  4. Don't create keyboard traps

    • Ensure users can navigate out of components
    • Implement proper focus management in modals
  5. 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

  1. Keyboard navigation

    • Tab through all interactive elements
    • Use arrow keys where appropriate
    • Test Escape, Enter, and Space keys
  2. Screen reader testing

    • Windows: NVDA (free), JAWS (commercial)
    • macOS: VoiceOver (built-in)
    • Mobile: TalkBack (Android), VoiceOver (iOS)
  3. 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>

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.