| |

Angular 88 🅰️ Accessibility (a11y) in Angular

⏱️ Estimated read: 19 min | |

Accessibility is not a feature you add at the end. It is a property of how a component is built. An accessible Angular application works with screen readers, keyboard navigation, and assistive technologies without requiring a separate version or a special mode. The framework provides the tools — the @angular/cdk/a11y package, attribute binding for ARIA, focus management utilities, and live announcers — but the discipline is in using them correctly .

The Angular CDK’s accessibility module is considered best-in-class among major frameworks . It was designed alongside Angular Material with the Angular team in close coordination. The API surface — FocusTrap, FocusMonitor, LiveAnnouncer, ListKeyManager, AriaDescriber — covers the recurring accessibility needs without overlap . For custom interactive components, using these primitives is the difference between an application that is accessible by design and one that is accessible by accident.

Key point: Accessibility in Angular has two layers. The template layer uses attr. binding for ARIA attributes, the role attribute for semantic meaning, and host bindings for component-level accessibility attributes . The runtime layer uses the CDK’s focus management, live announcers, and keyboard navigation utilities to handle the behaviors that ARIA attributes alone cannot express .


Why accessibility matters in Angular

Angular is a single-page application framework. The browser does not reload the page on navigation, and the framework renders the DOM dynamically. Both characteristics change what accessibility means compared to a traditional multi-page application.

The page title problem. In a traditional website, each page load sets a new <title>. In a single-page application, the title stays the same unless the developer changes it. Screen reader users rely on the page title to understand where they are. Angular’s Router provides a built-in title property that sets the document title on navigation .

The focus management problem. When a user navigates to a new route, the browser’s focus stays on the link that was clicked. A keyboard user must tab through the entire page to reach the new content. Accessible applications move focus to a meaningful location — the main heading, the first input, or a skip link — after navigation.

The dynamic content problem. Angular updates the DOM without a page reload. A screen reader that is reading the current page does not know that new content has appeared. Live regions (aria-live) tell the screen reader to announce the change. The CDK’s LiveAnnouncer service provides a programmatic API for this .

The custom component problem. A <div> with a click handler is not a button. It has no keyboard support, no role, and no accessible name. Accessible components use semantic HTML when possible and ARIA when not. The CDK’s ListKeyManager and FocusTrap handle the keyboard interaction and focus management that custom components require .

The trade-off. Accessibility adds constraints. Every custom component needs a role, a name, and keyboard support. Every dynamic update needs to be announced if it is important. The constraints are real, but they are the same constraints that make an application usable by everyone, not just users of assistive technology.


a. ARIA Attributes and Host Bindings

Angular templates can bind to ARIA attributes using the attr. prefix. The prefix is required because ARIA attributes are HTML attributes, not DOM properties. The attr. syntax tells Angular to set the attribute rather than the property .

<button [attr.aria-label]="actionLabel" (click)="save()">Save</button>

Static ARIA attributes require no extra syntax. They are written exactly as they appear in HTML :

<button aria-label="Save document">Save</button>

The attr. prefix is only necessary for dynamic bindings. When the expression evaluates to null, Angular removes the attribute entirely. This is useful for conditional ARIA attributes :

<div [attr.aria-disabled]="isDisabled ? true : null">Content</div>

The aria-disabled attribute is present only when isDisabled is true. When it is false, the attribute is removed.

For component-level accessibility attributes, the host property in the @Component decorator is the modern approach. It replaces the @HostBinding and @HostListener decorators, which exist only for backwards compatibility .

@Component({
  selector: 'app-progress-bar',
  template: '<div class="bar" [style.width.%]="value()"></div>',
  host: {
    'role': 'progressbar',
    'aria-valuemin': '0',
    'aria-valuemax': '100',
    '[attr.aria-valuenow]': 'value',
  },
})
export class ProgressBarComponent {
  value = input(0);
}

The role attribute sets the semantic meaning. The aria-valuemin and aria-valuemax attributes define the range. The [attr.aria-valuenow] binding updates the current value . The aria-label is set by the consumer of the component, not the component itself .

For bindings that accept structured values — like ariaLabelledByElements, which accepts an array of element references — property binding is used instead of attribute binding. This keeps the relationship synchronized when the referenced elements change :

<section role="dialog" [ariaLabelledByElements]="[dialogTitle, dialogDescription]">
  <h2 #dialogTitle>Attention</h2>
  <p #dialogDescription>Please review your answers.</p>
</section>

b. The CDK Accessibility Module

The @angular/cdk/a11y package provides the runtime utilities that ARIA attributes alone cannot express. The module is installed with npm install @angular/cdk and imported as A11yModule .

FocusMonitor is an injectable service that reports how an element received focus. It distinguishes between 'mouse', 'keyboard', 'touch', 'program', and null (lost focus) . This matters because the visual indicator of focus should differ depending on how the user is interacting with the page. A keyboard user needs a visible focus ring. A mouse user does not.

import { FocusMonitor, FocusOrigin } from '@angular/cdk/a11y';

export class CustomButtonComponent implements AfterViewInit, OnDestroy {
  private focusMonitor = inject(FocusMonitor);
  private elementRef = inject(ElementRef);

  ngAfterViewInit() {
    this.focusMonitor.monitor(this.elementRef, true).subscribe((origin: FocusOrigin) => {
      if (origin === 'keyboard') {
        this.elementRef.nativeElement.classList.add('keyboard-focused');
      } else {
        this.elementRef.nativeElement.classList.remove('keyboard-focused');
      }
    });
  }

  ngOnDestroy() {
    this.focusMonitor.stopMonitoring(this.elementRef);
  }
}

The monitor method returns an observable that emits the focus origin whenever the focus state changes. The checkChildren parameter extends monitoring to descendant elements .

FocusTrap is a directive that constrains Tab key focus within an element. It is used for modal dialogs, where focus must not escape to the background content .

<div class="dialog" cdkTrapFocus [cdkTrapFocusAutoCapture]="true">
  <h2>Dialog Title</h2>
  <button>Close</button>
</div>

The cdkTrapFocus directive traps Tab and Shift+Tab within the element. The cdkTrapFocusAutoCapture option automatically focuses the first tabbable element when the trap initializes . The cdkFocusInitial attribute marks the element that should receive focus on initialization. The cdkFocusRegionStart and cdkFocusRegionEnd attributes define the region within which focus is trapped .

LiveAnnouncer is a service for announcing messages to screen reader users. It uses an aria-live region to communicate changes that are not visible on screen .

import { LiveAnnouncer } from '@angular/cdk/a11y';

export class NotificationComponent {
  private liveAnnouncer = inject(LiveAnnouncer);

  announceSave() {
    this.liveAnnouncer.announce('Your changes have been saved', 'polite');
  }

  announceError(message: string) {
    this.liveAnnouncer.announce(message, 'assertive');
  }
}

The 'polite' politeness setting waits for the screen reader to finish its current utterance. The 'assertive' setting interrupts immediately. Use 'polite' for most announcements and 'assertive' only for critical errors .

ListKeyManager manages the active option in a list of items based on keyboard interaction. It is intended for components with role="menu" or role="listbox". There are two variants: FocusKeyManager for options that receive browser focus directly, and ActiveDescendantKeyManager for options marked as active via aria-activedescendant .


c. Router Integration and Page Titles

Angular’s Router provides a built-in title property for defining unique page titles. This is the simplest accessibility improvement a single-page application can make .

const routes: Routes = [
  { path: 'shop', component: ShopComponent, title: 'Our Shop – My App' },
  { path: 'about', component: AboutComponent, title: 'Our Story – My App' },
  { path: 'locate', component: LocationComponent, title: 'Find Us – My App' },
];

The Router uses the Title service internally to set the document title on navigation. The title is the first thing a screen reader announces when a page loads, so a unique, descriptive title is critical for users who cannot see the page content .

For more complex title logic, a custom TitleStrategy can be provided. The TitleStrategy is an abstract class that the Router uses to build and set the title. The default implementation uses the title property from the route configuration. A custom strategy can combine the route title with application-specific suffixes or prefixes .

The Router also emits navigation events that can be used to manage focus. After navigation completes, the application can move focus to the main heading or the first interactive element. This is not built into the Router, but it can be implemented with a subscription to NavigationEnd and a reference to the main content element.


Complete Example Session

This session builds an accessible modal dialog, a custom progress bar, and a notification service using the CDK accessibility module.

// ============================================
// PART 1: THE ACCESSIBLE MODAL DIALOG
// ============================================

import { Component, input, output, viewChild, ElementRef } from '@angular/core';
import { A11yModule } from '@angular/cdk/a11y';

@Component({
  selector: 'app-modal',
  imports: [A11yModule],
  template: `
    <div class="modal-backdrop" (click)="close.emit()">
      <div
        class="modal"
        role="dialog"
        aria-modal="true"
        [attr.aria-labelledby]="titleId"
        [attr.aria-describedby]="descriptionId"
        cdkTrapFocus
        [cdkTrapFocusAutoCapture]="true"
        (click)="$event.stopPropagation()"
      >
        <h2 [id]="titleId">{{ title() }}</h2>
        <p [id]="descriptionId">{{ description() }}</p>
        <ng-content />
        <button (click)="close.emit()">Close</button>
      </div>
    </div>
  `,
})
export class ModalComponent {
  title = input.required<string>();
  description = input.required<string>();
  close = output<void>();

  titleId = `modal-title-${crypto.randomUUID()}`;
  descriptionId = `modal-desc-${crypto.randomUUID()}`;
}

The role="dialog" and aria-modal="true" attributes identify the element as a modal dialog. The aria-labelledby and aria-describedby attributes connect the dialog to its title and description. The cdkTrapFocus directive traps Tab focus within the dialog, and cdkTrapFocusAutoCapture moves focus into the dialog on initialization .

// ============================================
// PART 2: THE ACCESSIBLE PROGRESS BAR
// ============================================

import { Component, input } from '@angular/core';

@Component({
  selector: 'app-progress-bar',
  template: '<div class="bar" [style.width.%]="value()"></div>',
  host: {
    'role': 'progressbar',
    'aria-valuemin': '0',
    'aria-valuemax': '100',
    '[attr.aria-valuenow]': 'value',
    '[attr.aria-valuetext]': 'valueText()',
  },
})
export class ProgressBarComponent {
  value = input(0);
  valueText = input<string | null>(null);
}

The role="progressbar" identifies the element as a progress bar. The aria-valuemin and aria-valuemax attributes define the range. The [attr.aria-valuenow] binding updates the current value. The [attr.aria-valuetext] binding provides a human-readable version of the value for screen readers .

// ============================================
// PART 3: THE LIVE ANNOUNCER SERVICE
// ============================================

import { Injectable, inject } from '@angular/core';
import { LiveAnnouncer } from '@angular/cdk/a11y';

@Injectable({ providedIn: 'root' })
export class NotificationService {
  private liveAnnouncer = inject(LiveAnnouncer);

  success(message: string): void {
    this.liveAnnouncer.announce(message, 'polite');
  }

  error(message: string): void {
    this.liveAnnouncer.announce(message, 'assertive');
  }
}

The LiveAnnouncer service announces messages to screen reader users. The 'polite' setting is used for success messages, and 'assertive' for errors. The announcement is made through an aria-live region that is managed by the CDK .

// ============================================
// PART 4: THE FOCUS MONITOR FOR A CUSTOM BUTTON
// ============================================

import { Component, ElementRef, inject, AfterViewInit, OnDestroy } from '@angular/core';
import { FocusMonitor, FocusOrigin } from '@angular/cdk/a11y';

@Component({
  selector: 'app-custom-button',
  template: '<button #btn (click)="handleClick()"><ng-content /></button>',
  styles: [`
    .keyboard-focused {
      outline: 2px solid blue;
      outline-offset: 2px;
    }
  `],
})
export class CustomButtonComponent implements AfterViewInit, OnDestroy {
  private focusMonitor = inject(FocusMonitor);
  private elementRef = inject(ElementRef);

  ngAfterViewInit() {
    this.focusMonitor.monitor(this.elementRef, true).subscribe((origin: FocusOrigin) => {
      if (origin === 'keyboard') {
        this.elementRef.nativeElement.querySelector('button')?.classList.add('keyboard-focused');
      } else {
        this.elementRef.nativeElement.querySelector('button')?.classList.remove('keyboard-focused');
      }
    });
  }

  ngOnDestroy() {
    this.focusMonitor.stopMonitoring(this.elementRef);
  }

  handleClick() {
    // Button logic
  }
}

The FocusMonitor reports how the button received focus. When the origin is 'keyboard', the component adds a visual focus indicator. When the origin is 'mouse', the indicator is removed. This is the standard pattern for focus-visible behavior .

// ============================================
// PART 5: THE ROUTER TITLE CONFIGURATION
// ============================================

import { Routes } from '@angular/router';

export const routes: Routes = [
  { path: '', redirectTo: '/shop', pathMatch: 'full' },
  { path: 'shop', component: ShopComponent, title: 'Our Shop – My App' },
  { path: 'about', component: AboutComponent, title: 'Our Story – My App' },
  { path: 'locate', component: LocationComponent, title: 'Find Us – My App' },
];

The title property on each route sets the document title on navigation. The Router uses the Title service internally to update the <title> element .

// ============================================
// PART 6: THE SKIP LINK
// ============================================

@Component({
  selector: 'app-root',
  template: `
    <a href="#main-content" class="skip-link">Skip to main content</a>
    <header>...</header>
    <main id="main-content" tabindex="-1">
      <router-outlet />
    </main>
  `,
  styles: [`
    .skip-link {
      position: absolute;
      left: -9999px;
    }
    .skip-link:focus {
      left: 0;
      top: 0;
      background: white;
      padding: 1rem;
      z-index: 100;
    }
  `],
})
export class AppComponent {}

The skip link allows keyboard users to bypass the navigation and jump directly to the main content. The link is visually hidden until it receives focus, at which point it becomes visible. The tabindex="-1" on the main element allows it to receive focus programmatically .

// ============================================
// PART 7: THE KEYBOARD NAVIGATION WITH LIST KEY MANAGER
// ============================================

import { Component, viewChildren, AfterViewInit, OnDestroy } from '@angular/core';
import { FocusKeyManager, FocusableOption } from '@angular/cdk/a11y';

@Component({
  selector: 'app-menu-item',
  template: '<button (click)="select()">{{ label }}</button>',
})
export class MenuItemComponent implements FocusableOption {
  label = '';

  focus() {
    // Focus the button element
  }

  select() {
    // Handle selection
  }
}

@Component({
  selector: 'app-menu',
  template: `
    <div role="menu" (keydown)="handleKeydown($event)">
      @for (item of items; track item.id) {
        <app-menu-item [label]="item.label" />
      }
    </div>
  `,
})
export class MenuComponent implements AfterViewInit, OnDestroy {
  items = viewChildren(MenuItemComponent);
  private keyManager!: FocusKeyManager<MenuItemComponent>;

  ngAfterViewInit() {
    this.keyManager = new FocusKeyManager(this.items()).withWrap();
  }

  handleKeydown(event: KeyboardEvent) {
    this.keyManager.onKeydown(event);
  }

  ngOnDestroy() {
    // Cleanup
  }
}

The FocusKeyManager manages the active item in a list. It handles arrow key navigation and wraps around at the ends when withWrap() is called. Each item implements the FocusableOption interface, which requires a focus() method .

// ============================================
// PART 8: THE CONDITIONAL ARIA ATTRIBUTE
// ============================================

@Component({
  selector: 'app-form-field',
  template: `
    <label [attr.for]="fieldId">{{ label }}</label>
    <input
      [id]="fieldId"
      [attr.aria-required]="required ? true : null"
      [attr.aria-invalid]="invalid ? true : null"
      [attr.aria-describedby]="invalid ? errorId : null"
    />
    @if (invalid) {
      <span [id]="errorId" role="alert">{{ errorMessage }}</span>
    }
  `,
})
export class FormFieldComponent {
  label = '';
  required = false;
  invalid = false;
  errorMessage = '';
  fieldId = `field-${crypto.randomUUID()}`;
  errorId = `error-${crypto.randomUUID()}`;
}

The aria-required and aria-invalid attributes are added conditionally. When the expression evaluates to null, Angular removes the attribute. The role="alert" on the error message causes screen readers to announce it immediately when it appears .

// ============================================
// PART 9: THE FOCUS TRAP FOR A DRAWER
// ============================================

@Component({
  selector: 'app-drawer',
  imports: [A11yModule],
  template: `
    @if (open()) {
      <div class="drawer" cdkTrapFocus [cdkTrapFocusAutoCapture]="true">
        <h2>Navigation</h2>
        <nav>
          <a routerLink="/shop">Shop</a>
          <a routerLink="/about">About</a>
        </nav>
        <button (click)="close()">Close</button>
      </div>
    }
  `,
})
export class DrawerComponent {
  open = signal(false);
  close() {
    this.open.set(false);
  }
}

The cdkTrapFocus directive constrains Tab focus within the drawer when it is open. The cdkTrapFocusAutoCapture option moves focus into the drawer when it opens. This is the pattern for side drawers, modals, and any overlay that requires focus to be contained .

// ============================================
// PART 10: THE COMPLETE TEST
// ============================================

import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ModalComponent } from './modal.component';

describe('ModalComponent', () => {
  let fixture: ComponentFixture<ModalComponent>;

  beforeEach(() => {
    TestBed.configureTestingModule({ imports: [ModalComponent] });
    fixture = TestBed.createComponent(ModalComponent);
    fixture.componentRef.setInput('title', 'Test Dialog');
    fixture.componentRef.setInput('description', 'Test Description');
    fixture.detectChanges();
  });

  it('has the correct ARIA attributes', () => {
    const dialog = fixture.nativeElement.querySelector('[role="dialog"]');
    expect(dialog.getAttribute('aria-modal')).toBe('true');
    expect(dialog.getAttribute('aria-labelledby')).toBeTruthy();
    expect(dialog.getAttribute('aria-describedby')).toBeTruthy();
  });

  it('traps focus within the dialog', () => {
    const dialog = fixture.nativeElement.querySelector('[role="dialog"]');
    expect(dialog.hasAttribute('cdktrapfocus')).toBe(true);
  });
});

The test verifies that the modal has the correct ARIA attributes and that the focus trap is applied. Accessibility testing should include both automated checks (like axe or Lighthouse) and manual verification with a screen reader .


Quick Reference

The ARIA Binding Syntax

SyntaxPurpose
[attr.aria-label]Dynamic ARIA attribute
aria-label="..."Static ARIA attribute
[attr.aria-disabled]="true : null"Conditional ARIA attribute
[ariaLabelledByElements]Property binding for element references

The CDK A11y Module

UtilityPurpose
FocusMonitorDetect how an element received focus
FocusTrapConstrain Tab focus within an element
LiveAnnouncerAnnounce messages to screen readers
ListKeyManagerManage keyboard navigation in lists
InteractivityCheckerCheck element interactivity states
AriaDescriberManage aria-describedby relationships

The Focus Origins

OriginMeaning
'mouse'Focused via mouse click
'keyboard'Focused via keyboard navigation
'touch'Focused via touch interaction
'program'Focused programmatically
nullElement lost focus

The CDK Focus Directives

DirectivePurpose
cdkTrapFocusTrap Tab focus within an element
cdkTrapFocusAutoCaptureAuto-focus first tabbable element
cdkFocusInitialMark initial focus target
cdkFocusRegionStartStart of focus region
cdkFocusRegionEndEnd of focus region
cdkMonitorElementFocusMonitor focus on an element
cdkMonitorSubtreeFocusMonitor focus on subtree

The Router Title Configuration

PropertyPurpose
title on routeSet document title on navigation
TitleStrategyCustom title building logic
Title serviceProgrammatic title management

Best Practices

✅ Do This:

<!-- Use attr. for dynamic ARIA attributes -->
<button [attr.aria-label]="label">Save</button>              <!-- ✅ -->
// Use FocusMonitor to detect focus origin
this.focusMonitor.monitor(this.elementRef).subscribe(...)    // ✅
// Use LiveAnnouncer for dynamic messages
this.liveAnnouncer.announce('Saved', 'polite');              // ✅
// Set unique page titles on routes
{ path: 'shop', title: 'Shop – My App' }                     // ✅
<!-- Use cdkTrapFocus for modals and drawers -->
<div cdkTrapFocus [cdkTrapFocusAutoCapture]="true">           <!-- ✅ -->

❌ Don’t Do This:

<!-- Don't use a div as a button -->
<div (click)="save()">Save</div>                             <!-- ❌ -->
// Don't rely on mouse hover for essential information
<div (mouseover)="showTooltip()">Info</div>                  // ❌
<!-- Don't forget the accessible name -->
<button>📁</button>  <!-- no aria-label or text -->           <!-- ❌ -->
// Don't skip the focus monitor cleanup
// Monitor without stopMonitoring leaks subscriptions.        // ❌
<!-- Don't use role="button" without keyboard support -->
<div role="button" (click)="save()">Save</div>               <!-- ❌ -->

Common Pitfalls

PitfallWhy It HappensFix
Focus stays on clicked link after navigationNo focus managementMove focus to main content
Screen reader doesn’t announce changesNo live regionUse LiveAnnouncer
Modal focus escapes to backgroundNo focus trapUse cdkTrapFocus
Page title never changesNo title on routesAdd title to route config
Custom component has no keyboard supportNo key managerUse ListKeyManager
ARIA attribute not appliedMissing attr. prefixUse [attr.aria-*]

Real-World Examples

1. ARIA Label Binding

<button [attr.aria-label]="actionLabel">Save</button>

2. Conditional ARIA

<div [attr.aria-disabled]="disabled ? true : null">Content</div>

3. Progress Bar

host: { 'role': 'progressbar', '[attr.aria-valuenow]': 'value' }

4. Focus Trap

<div cdkTrapFocus [cdkTrapFocusAutoCapture]="true">...</div>

5. Live Announcer

this.liveAnnouncer.announce('Changes saved', 'polite');

6. Focus Monitor

this.focusMonitor.monitor(this.elementRef, true).subscribe(...)

7. Router Title

{ path: 'shop', title: 'Shop – My App' }

8. Skip Link

<a href="#main" class="skip-link">Skip to content</a>

9. List Key Manager

this.keyManager = new FocusKeyManager(this.items()).withWrap();

10. Modal Dialog

<div role="dialog" aria-modal="true" cdkTrapFocus>...</div>

Visual

The Two Layers of Angular Accessibility

┌──────────────────────────────────────────────┐
│  TEMPLATE LAYER                              │
│                                              │
│  attr.aria-*                                 │
│    └─ Dynamic ARIA attributes                │
│                                              │
│  role="..."                                  │
│    └─ Semantic meaning                       │
│                                              │
│  host: { ... }                               │
│    └─ Component-level ARIA                   │
│                                              │
├──────────────────────────────────────────────┤
│  RUNTIME LAYER (CDK)                         │
│                                              │
│  FocusMonitor                                │
│    └─ Detect focus origin                    │
│                                              │
│  FocusTrap                                   │
│    └─ Constrain Tab focus                    │
│                                              │
│  LiveAnnouncer                               │
│    └─ Screen reader messages                 │
│                                              │
│  ListKeyManager                              │
│    └─ Keyboard navigation                    │
│                                              │
└──────────────────────────────────────────────┘

The Focus Monitor Origins

┌──────────────────────────────────────────────┐
│  FocusOrigin VALUES                          │
│                                              │
│  'keyboard'                                  │
│    └─ Tab navigation                         │
│    └─ Requires visible focus indicator       │
│                                              │
│  'mouse'                                     │
│    └─ Click                                   │
│    └─ No focus ring needed                   │
│                                              │
│  'touch'                                     │
│    └─ Tap                                     │
│    └─ No focus ring needed                   │
│                                              │
│  'program'                                   │
│    └─ .focus() call                          │
│    └─ Depends on context                     │
│                                              │
│  null                                        │
│    └─ Element lost focus                     │
│                                              │
└──────────────────────────────────────────────┘

The Focus Trap for Modals

┌──────────────────────────────────────────────┐
│  FOCUS TRAP                                  │
│                                              │
│  Background page                             │
│    └─ Tab is trapped                         │
│                                              │
│  ┌────────────────────────────────────────┐  │
│  │  Modal (cdkTrapFocus)                  │  │
│  │    ├─ Tab cycles within modal          │  │
│  │    ├─ Shift+Tab cycles backward        │  │
│  │    └─ Focus cannot escape              │  │
│  └────────────────────────────────────────┘  │
│                                              │
│  Close button inside the trap.               │
│  Focus returns to trigger on close.          │
│                                              │
└──────────────────────────────────────────────┘

The Accessibility Testing Flow

┌──────────────────────────────────────────────┐
│  TESTING WORKFLOW                            │
│                                              │
│  Automated:                                  │
│    ├─ Lighthouse (Chrome DevTools)           │
│    ├─ axe DevTools extension                 │
│    └─ Unit tests for ARIA attributes         │
│                                              │
│  Manual:                                     │
│    ├─ Keyboard navigation (Tab, Enter)       │
│    ├─ Screen reader (VoiceOver, NVDA)        │
│    └─ Color contrast check                   │
│                                              │
│  Both are required.                          │
│  Automated tools catch ~30% of issues.       │
│                                              │
└──────────────────────────────────────────────┘

Summary

ItemValue
ARIA binding[attr.aria-*] for dynamic attributes
Static ARIAWritten as regular HTML attributes
Host bindinghost: { 'role': '...', '[attr.aria-*]': ... }
CDK package@angular/cdk/a11y
FocusMonitorDetect focus origin (mouse, keyboard, touch)
FocusTrapConstrain Tab focus within an element
LiveAnnouncerAnnounce messages to screen readers
ListKeyManagerKeyboard navigation in lists
Router titletitle property on routes
Skip linkFirst focusable element on the page
Politeness'polite' for status, 'assertive' for errors

Key takeaways:

  • ARIA attributes are bound with the attr. prefix. The prefix is required because ARIA attributes are HTML attributes, not DOM properties. Static attributes require no prefix. When the bound expression is null, the attribute is removed .
  • The CDK accessibility module provides the runtime utilities that ARIA alone cannot express. FocusMonitor detects how an element received focus. FocusTrap constrains Tab focus within a modal or drawer. LiveAnnouncer announces messages to screen readers. ListKeyManager handles keyboard navigation in custom lists .
  • Focus management is the most common accessibility gap in single-page applications. After navigation, focus should move to a meaningful location — the main heading or the first interactive element. A skip link allows keyboard users to bypass repetitive navigation .
  • Page titles must be unique and descriptive. Angular’s Router provides a built-in title property for this. The title is the first thing a screen reader announces when a page loads, and a generic title provides no value .
  • Live regions announce dynamic changes. When Angular updates the DOM without a page reload, screen readers do not know that new content has appeared. LiveAnnouncer uses an aria-live region to announce the change. Use 'polite' for status messages and 'assertive' for critical errors .
  • Focus traps are required for modals and drawers. When a dialog opens, focus must move into it and must not escape to the background content. The cdkTrapFocus directive handles this, and cdkTrapFocusAutoCapture moves focus into the dialog on initialization .
  • Accessibility requires both automated and manual testing. Lighthouse and axe catch about 30% of issues. The rest require keyboard navigation testing and screen reader verification. The CDK provides the primitives; the discipline is in using them correctly .

Remember: Accessibility in Angular is not a separate feature. It is a property of how components are built. ARIA attributes provide the semantic meaning. The CDK provides the runtime behavior. The Router provides the page titles. The discipline is in using all three together — marking components with the correct roles, managing focus when content changes, announcing updates that are not visible, and testing with the tools that real users rely on. The framework provides the tools. The application provides the experience.

Was this helpful?


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!