Angular CDK a11y — LiveAnnouncer and FocusMonitor
Permalink to "Angular CDK a11y — LiveAnnouncer and FocusMonitor"The Angular CDK’s @angular/cdk/a11y package provides small, well-tested utilities that most data UIs need: LiveAnnouncer for screen reader messages, FocusMonitor for knowing whether focus came from keyboard, mouse or code, FocusTrap for containing focus in dialogs, ListKeyManager for roving focus (covered separately), and InteractivityChecker for finding focusable elements. Using them instead of hand-rolled equivalents removes a whole class of bugs — lost announcements, focus rings on mouse click, escaping dialogs.
This guide belongs to Angular data components.
Spec reference
Permalink to "Spec reference"LiveAnnouncer.announce(message, politeness?, duration?)— writes to a single live region the CDK maintains in the document;politenessis'polite'or'assertive';clear()empties it.FocusMonitor.monitor(element, checkChildren)— emits the focus origin ('keyboard' | 'mouse' | 'touch' | 'program' | null) and adds classes likecdk-keyboard-focused.cdkTrapFocus— traps Tab within an element;cdkTrapFocusAutoCapturemoves focus in and restores it on destroy.- Relevant criteria: SC 4.1.3 (AA), SC 2.4.7 (AA), SC 2.4.3 (A), SC 2.1.2 (A).
When to use — and when not to
Permalink to "When to use — and when not to"Use LiveAnnouncer for every status message in an Angular data UI, rather than per-component aria-live elements. Use FocusMonitor when you need to know how focus arrived — for example, to show a strong focus ring only for keyboard focus, or to restore focus after an overlay with the correct origin.
Do not use FocusMonitor to hide focus for mouse users if your design system shows focus for everyone; :focus-visible in CSS is often enough. Do not use cdkTrapFocus for non-modal panels.
The misapplication: calling LiveAnnouncer.announce from every keystroke handler and from multiple components at once, producing overlapping messages. The CDK announcer is one region; coordinate calls through a service.
Annotated code example
Permalink to "Annotated code example"@Injectable({ providedIn: 'root' })
export class DataAnnouncer {
private pending = new Map<string, string>(); private timer?: ReturnType<typeof setTimeout>;
constructor(private live: LiveAnnouncer) {}
// Merge bursts by key so sort + count become one message (SC 4.1.3)
announce(message: string, key = message, politeness: AriaLivePoliteness = 'polite') {
this.pending.set(key, message);
clearTimeout(this.timer);
this.timer = setTimeout(() => {
const text = [...this.pending.values()].join(' '); this.pending.clear();
this.live.clear(); // allow identical messages to re-announce
this.live.announce(text, politeness);
}, 250);
}
}
@Component({ selector: 'app-filter-button', template: `<button #btn (click)="open()">Filter Amount</button>` })
export class FilterButton implements AfterViewInit, OnDestroy {
@ViewChild('btn') btn!: ElementRef<HTMLButtonElement>;
private origin: FocusOrigin = null;
constructor(private fm: FocusMonitor, private dialog: Dialog) {}
ngAfterViewInit() { this.fm.monitor(this.btn).subscribe(o => this.origin = o); } // remember how focus arrived
open() {
const ref = this.dialog.open(FilterPanel, { ariaLabelledBy: 'filter-h', restoreFocus: false });
ref.closed.subscribe(() => this.fm.focusVia(this.btn, this.origin ?? 'program')); // restore with origin (SC 2.4.3)
}
ngOnDestroy() { this.fm.stopMonitoring(this.btn); }
}
/* Strong focus ring for keyboard focus (SC 2.4.7) */
.cdk-keyboard-focused { outline: 3px solid var(--focus); outline-offset: 2px; }
Keyboard & AT behaviour
Permalink to "Keyboard & AT behaviour"| Utility | Behaviour | What users experience |
|---|---|---|
| LiveAnnouncer | Message in a live region | “Sorted by Amount, ascending. 240 invoices.” |
| FocusMonitor | Origin-aware classes | Keyboard users see a strong ring |
| cdkTrapFocus | Tab cycles within | Cannot Tab out of a modal panel |
| Dialog restore | Focus returns to trigger | Same place, same focus style |
| InteractivityChecker | Finds focusable cells | Roving focus skips hidden items |
Integration context
Permalink to "Integration context"Announcement design follows announcement queues for competing messages. Dialog focus behaviour is discussed in native dialog versus custom focus traps. Roving focus with the CDK is covered in Angular CDK ListKeyManager for custom grids.
Gotchas
Permalink to "Gotchas"Announcer duration. announce accepts a duration after which the message is cleared; very short durations can clear text before slower readers speak it. Leave the default or use several seconds.
Server-side rendering. LiveAnnouncer creates its region in the browser; with SSR, announcements only work after hydration, which is usually fine for user-driven events.
FocusMonitor cleanup. Always stopMonitoring in ngOnDestroy to avoid leaks.
Common mistakes
Permalink to "Common mistakes"- Per-component aria-live elements alongside LiveAnnouncer.
- Every message assertive.
- Focus traps on non-modal panels.
- Forgetting to restore focus after dialogs.
Adoption checklist for teams
Permalink to "Adoption checklist for teams"- Provide a
DataAnnouncerservice wrapping LiveAnnouncer with keys and merging. - Lint against
aria-liveattributes in component templates. - Use CDK Dialog (or Material Dialog) for modal panels; never hand-rolled traps.
- Add
.cdk-keyboard-focusedstyles to the design system, or rely on:focus-visibleconsistently. - Test announcements with NVDA and VoiceOver on sort, filter and errors.
Version and upgrade notes
Permalink to "Version and upgrade notes"CDK a11y APIs have been stable for many versions. Newer Angular releases favour standalone components and signals; the utilities work the same way, and signal-based state can drive announcements through effects (see the signals guide).
Worked example
Permalink to "Worked example"A reporting app had seven components with their own aria-live regions plus occasional LiveAnnouncer calls. Users heard overlapping or missing messages. The team introduced a DataAnnouncer service with keys, removed all template live regions, moved modal filter panels to CDK Dialog with focus restoration via FocusMonitor, and styled keyboard focus with .cdk-keyboard-focused. Announcement bugs stopped, and keyboard users regained their place after closing filters.
Testing checklist
Permalink to "Testing checklist"Key points
Permalink to "Key points"- Inject one
LiveAnnouncerand route every data message through it, choosing politeness per message. - Use
FocusMonitorto style keyboard focus differently from mouse focus without removing indicators. - Clear announcements on route changes so stale messages are not read on the next screen.
Design system notes
Permalink to "Design system notes"Expose the announcer service, focus styles and dialog defaults from the design system’s Angular package, so every product uses the same behaviour and wording.
FAQ
Permalink to "FAQ"Is LiveAnnouncer polite by default?
Yes; pass ‘assertive’ for urgent messages.
Can LiveAnnouncer announce the same message twice?
Clear it first (or use a slightly different message) so screen readers detect the change.
Should I use FocusMonitor or :focus-visible?
:focus-visible is usually enough for styling. Use FocusMonitor when you need the origin in code, such as restoring focus the same way.
Does CDK Dialog restore focus automatically?
Yes, by default; disable it only when you restore focus yourself.
Can I use these utilities with non-Material components?
Yes. The CDK is independent of Material’s styling.
How do I test announcements?
Spy on LiveAnnouncer.announce in unit tests, and use speech logs for end-to-end checks.
What about InteractivityChecker?
Use it when building custom focus logic, such as skipping hidden cells in a roving grid.
Where does LiveAnnouncer put its region?
It creates a visually hidden live element at the end of the document body, shared by all calls. Do not create additional regions for the same purpose.
Related
Permalink to "Related"- ListKeyManager for custom grids — roving focus
- Angular signals and live announcements — signal-driven messages
- Native dialog versus custom traps — modal behaviour