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-describedbypointing at the message element, and togglearia-invalidfrom the control'sinvalid && touchedstate - 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.