AccessScanRun a free scan

Guide

Angular Accessibility: CDK a11y Utilities, Focus, and SPA Routing

Angular ships more accessibility infrastructure than almost any other framework, and most teams never touch it. The @angular/cdk/a11y package solves the hard parts of Angular accessibility, focus trapping, live announcements, and focus monitoring, while the framework's own routing quietly breaks the one thing single-page apps get wrong most often: moving focus when the URL changes.

This guide is practical and Angular-specific. We will wire up the CDK a11y utilities, fix route-change focus, build forms screen readers can actually use, and mark out where the framework helps you and where you are still on your own.

What the Angular CDK a11y package actually gives you

The accessibility tooling lives in @angular/cdk/a11y, imported via the standalone directives or the legacy A11yModule. It is the same code that powers Angular Material, so it is battle-tested across dialogs, menus, and overlays. The four pieces you will reach for most are LiveAnnouncer, FocusTrap (with the cdkTrapFocus directive), FocusMonitor, and the cdkAriaLive directive.

None of this is automatic. The CDK gives you correct primitives; you still decide when focus moves, what gets announced, and how forms are labelled. That division of labor is the whole game in Angular accessibility, and it maps directly onto the WCAG 2.2 success criteria your app is measured against.

Focus management with FocusTrap and FocusMonitor

When you open a modal, menu, or off-canvas panel, keyboard focus must stay inside it and return to the trigger on close. The cdkTrapFocus directive handles the containment:

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

cdkTrapFocusAutoCapture moves focus into the trap on open and restores it to the previously focused element on destroy, which is exactly what assistive-tech users expect. For programmatic control, inject ConfigurableFocusTrapFactory and call create(element) to get a trap you can focusInitialElement() or destroy() yourself. Mark the preferred starting point with the cdkFocusInitial attribute so focus does not land on a stray close button.

FocusMonitor solves a subtler problem: telling keyboard focus apart from mouse focus. Call this.focusMonitor.monitor(el) and Angular adds .cdk-focused plus .cdk-keyboard-focused or .cdk-mouse-focused classes, so you can show a strong focus ring for keyboard users without flashing one on every click. Always stopMonitoring(el) in ngOnDestroy to avoid leaks. Pair this with broader keyboard accessibility work so every interactive control is reachable and operable.

Announcing dynamic changes with LiveAnnouncer

Angular re-renders the DOM constantly, and screen readers miss most of it. LiveAnnouncer pushes text into a visually hidden ARIA live region so updates are spoken:

constructor(private live: LiveAnnouncer) {} then this.live.announce('Showing 12 of 240 results', 'polite');

Use 'polite' for status updates and 'assertive' only for errors that must interrupt. For regions that update on their own, the cdkAriaLive directive is cleaner. Put it on the element and the CDK watches it with a MutationObserver:

<span cdkAriaLive="polite">{{ resultCount }} results</span>

  • Call announce() once per logical event, not per keystroke, or you will flood the user
  • Avoid two live regions describing the same change, which causes double-speak
  • Clear long-running announcements with clear() before navigating away

The SPA blind spot: focus on route change

This is the single most common Angular accessibility defect. When a user activates a routerLink, the view swaps but focus stays on the link they clicked. Sighted users see a new page; keyboard and screen reader users get no signal and are stranded at the top of the old DOM order. There is no automatic fix, and it directly affects WCAG 2.4.3 Focus Order (Level A).

Subscribe to NavigationEnd once, in a root component or a route-focus service, then move focus deliberately:

this.router.events.pipe(filter(e => e instanceof NavigationEnd)).subscribe(() => { this.title.setTitle(routeTitle); const target = document.querySelector('h1, [role=main]') as HTMLElement; target?.setAttribute('tabindex', '-1'); target?.focus(); this.live.announce(routeTitle); });

Setting tabindex="-1" lets a non-interactive heading receive programmatic focus without joining the tab order. Announcing the new title via LiveAnnouncer covers the gap between focus landing and the screen reader catching up. A skip link that targets the same element gives keyboard users a fast path past the nav on every route.

Accessible forms in Angular

Reactive and template-driven forms are equally capable of being inaccessible. The framework does not generate labels or wire error messages to inputs, so you must do both.

  • Pair every control with a real <label for> or wrap it; placeholder text is not a label and disappears on input
  • Bind errors with aria-describedby pointing at the message element, and toggle aria-invalid from the control's invalid && touched state
  • Render validation messages in an aria-live="polite" region so they are announced when they appear, not just shown
  • Group radio buttons and related checkboxes in a <fieldset> with a <legend> so the group purpose is spoken
  • Never disable the submit button as the only error feedback; explain what is wrong in text

WCAG 2.2 added 3.3.7 Redundant Entry (Level A) and 3.3.8 Accessible Authentication (Minimum) (Level AA), both of which hit multi-step Angular forms and login flows hard. Do not force users to re-type data they already entered, and do not require a cognitive test such as transcribing a code with no accessible alternative. Field labels and error text must also clear 4.5:1 contrast against their background.

Testing and shipping with confidence

Wire axe-core into your component tests to fail the build on regressions, then run keyboard-only and screen reader passes on critical flows, since no automated tool catches everything, typically a third to half of issues. For full-page coverage across deployed routes, run a free scan with AccessScan to surface contrast, labelling, and structure problems.

The stakes are concrete: the European Accessibility Act has applied since 28 June 2025, with WCAG 2.2 Level AA via EN 301 549 as the baseline for consumer-facing digital products. Treat the CDK a11y utilities as your foundation, fix route-change focus everywhere, and verify the result rather than trusting that the framework handled it.

Check your site against AccessScan

See your issues ranked by impact in seconds — free.

Run a free accessibility scan

FAQ

Does Angular have built-in accessibility support?

Angular ships the Angular CDK (Component Dev Kit), whose @angular/cdk/a11y package provides production-ready accessibility primitives: LiveAnnouncer, FocusTrap, FocusMonitor, and the cdkAriaLive and cdkTrapFocus directives. They are built for Angular's dependency injection and change detection. The CDK does not, however, fix route-change focus or label your forms for you. Those remain your responsibility.

How do I move focus after a route change in an Angular SPA?

Subscribe to the Router's NavigationEnd events, then programmatically move focus to a skip target or the page's main heading. Add tabindex="-1" to that element so it can receive focus, call .focus() on it, and announce the new page title with LiveAnnouncer so screen reader users know navigation completed. Without this, focus stays on the clicked link and the rest of the app is silent.

What is the difference between LiveAnnouncer and cdkAriaLive?

LiveAnnouncer is an injectable service you call imperatively, for example this.live.announce('5 results found', 'polite'). cdkAriaLive is a directive you place on an element whose text content changes; the CDK watches it with a MutationObserver and announces updates automatically. Use the service for one-off events like form submission, and the directive for regions whose content updates on their own, like a results counter.

Do Angular Material components meet WCAG out of the box?

Angular Material components are built on the CDK and follow ARIA authoring practices, so they start far ahead of hand-rolled widgets. But accessibility still depends on how you use them: missing mat-label text, insufficient color contrast in a custom theme, or icon-only buttons with no aria-label will all fail WCAG regardless of the underlying component. Treat Material as a strong foundation, not a guarantee, and test the rendered result.

How can I test Angular accessibility automatically?

Combine unit-level checks with whole-page scans. Tools like axe-core can run inside Karma or Jest tests against rendered component fixtures to catch issues per component. For full-page coverage including dynamic states, run a scanner such as AccessScan against deployed routes. Automated tools catch only about a third to half of issues, so pair them with keyboard-only and screen reader passes on your key flows.

More guides