Angular Accessibility Guide 2026: WCAG Compliance for Angular Apps
Angular has the most built-in accessibility tooling of any major frontend framework — the Angular CDK's a11y module includes focus management utilities, live announcer service, and keyboard event handling. But CDK availability doesn't mean Angular apps are accessible by default. This guide covers where Angular apps fail WCAG 2.1 AA and the specific Angular patterns that fix them.
Quick Summary
- Angular CDK's a11y module handles focus trapping, live announcements, and keyboard navigation
- Angular Router doesn't announce navigation to screen readers by default — fix with LiveAnnouncer
- Angular Material components are ARIA-compliant out of the box; custom components often aren't
- axe-core integrates with Karma and Jest for automated accessibility testing in CI
- Angular's strict template checking catches some ARIA errors at compile time
The Most Common Angular Accessibility Mistakes
(click) on a <div> or <span> excludes keyboard and screen reader users. Use <button> for actions and <a routerLink> for navigation. If you must use a custom element, apply role="button", tabindex="0", and (keydown.enter)/(keydown.space) event bindings — but a semantic <button> is always preferable and requires no extra work.
Angular Router's NavigationEnd fires when routing completes, but it doesn't announce the change to screen readers. Inject LiveAnnouncer from @angular/cdk/a11y and call liveAnnouncer.announce() in a NavigationEnd subscription to notify screen reader users that the page changed.
Angular's change detection updates the DOM silently. Search results, form errors, and notification banners that appear reactively won't be announced to screen readers unless wrapped in an aria-live region or announced via Angular CDK's LiveAnnouncer service.
Angular Reactive Forms and Template-Driven Forms don't automatically associate labels with inputs. Use <label [for]="controlId"> with a matching [id] on the input, or [attr.aria-label] for inputs where a visible label isn't possible. placeholder is not an accessible label.
Angular's *ngIf-based modal patterns don't trap focus by default. Use Angular CDK's FocusTrap directive (cdkTrapFocus) on your modal container, and cdkTrapFocusAutoCapture to move focus inside when the modal opens. Return focus to the trigger element on close.
routerLinkActive adds a CSS class but doesn't set aria-current="page" on the active navigation link. Add [attr.aria-current]="isActive ? 'page' : null" to active navigation links so screen reader users know which page they're on.
Angular CDK a11y Module
The Angular CDK's accessibility module is one of Angular's biggest advantages for building accessible UIs. Install it once and use across your app:
LiveAnnouncer — Announce Route Changes
import { LiveAnnouncer } from '@angular/cdk/a11y'
import { Router, NavigationEnd } from '@angular/router'
@Component({ ... })
export class AppComponent implements OnInit {
constructor(
private router: Router,
private liveAnnouncer: LiveAnnouncer,
private titleService: Title
) {}
ngOnInit() {
this.router.events.pipe(
filter(e => e instanceof NavigationEnd)
).subscribe(() => {
const title = this.titleService.getTitle()
this.liveAnnouncer.announce(`Navigated to ${title}`, 'polite')
})
}
}FocusTrap — Modal Dialogs
<!-- dialog.component.html -->
<div
role="dialog"
aria-modal="true"
[attr.aria-labelledby]="titleId"
cdkTrapFocus
cdkTrapFocusAutoCapture
>
<h2 [id]="titleId">{{ title }}</h2>
<div class="dialog-body">
<ng-content></ng-content>
</div>
<button (click)="close()">Close</button>
</div>FocusMonitor — Track Focus State
import { FocusMonitor } from '@angular/cdk/a11y'
@Component({ ... })
export class ButtonComponent implements OnInit, OnDestroy {
constructor(
private focusMonitor: FocusMonitor,
private el: ElementRef
) {}
ngOnInit() {
// Detects keyboard vs mouse focus — show focus ring only for keyboard
this.focusMonitor.monitor(this.el, true).subscribe(origin => {
this.showFocusRing = origin === 'keyboard'
})
}
ngOnDestroy() {
this.focusMonitor.stopMonitoring(this.el)
}
}Automated Accessibility Testing in Angular
Use axe-core with your existing test setup to catch WCAG violations automatically in CI:
Jest + axe-core
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { axe } from 'jest-axe'
import { MyFormComponent } from './my-form.component'
describe('MyFormComponent accessibility', () => {
let fixture: ComponentFixture<MyFormComponent>
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [MyFormComponent]
}).compileComponents()
fixture = TestBed.createComponent(MyFormComponent)
fixture.detectChanges()
})
it('should have no accessibility violations', async () => {
const results = await axe(fixture.nativeElement)
expect(results).toHaveNoViolations()
})
})For Playwright e2e tests, use @axe-core/playwright to run accessibility checks on full rendered pages with real CSS applied — catching contrast violations that unit tests miss.
Angular Material: What's Accessible, What's Not
Angular Material components are generally WCAG 2.1 AA compliant out of the box. But there are important caveats:
Angular Accessibility Checklist
Scan Your Angular App for WCAG Issues
Run an automated WCAG 2.1 scan on your Angular production URL. Catches contrast violations, missing landmarks, and ARIA errors that don't appear in unit tests.
Sponsored
Find Angular accessibility issues in your browser
Deque Axe DevTools Pro integrates with Chrome and Firefox DevTools — run WCAG checks on rendered Angular components as you develop, before CI.