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; politeness is 'polite' or 'assertive'; clear() empties it.
  • FocusMonitor.monitor(element, checkChildren) — emits the focus origin ('keyboard' | 'mouse' | 'touch' | 'program' | null) and adds classes like cdk-keyboard-focused.
  • cdkTrapFocus — traps Tab within an element; cdkTrapFocusAutoCapture moves 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).
CDK a11y utilities in a data UI Layers: LiveAnnouncer for results and errors; FocusMonitor for focus origin and styles; FocusTrap and Dialog for modal overlays; ListKeyManager for roving focus in lists and grids; InteractivityChecker for finding focusable elements. CDK a11y utilities in a data UILiveAnnouncerone live region; polite and assertive messagesFocusMonitorfocus origin; keyboard focus classesFocusTrap and Dialogcontain focus; restore on closeListKeyManagerroving focus for lists and gridsInteractivityCheckeris this element focusable and visible?
Each utility replaces code teams often get subtly wrong.

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.

Hand-rolled versus CDK utilities Comparison: hand-rolled live regions and focus logic are created late, duplicated and inconsistent; CDK utilities provide one tested region, focus origin tracking and correct trapping. Hand-rolled versus CDK utilities✗ Hand-rolledaria-live div per componentRegion created when needed (often silent)Focus rings on every click or noneCustom Tab trap with edge casesFocus lost after overlay closes✓ CDK a11yOne LiveAnnouncer regionRegion exists before messagesFocus origin known; styled accordinglycdkTrapFocus with restoreRestore to recorded origin

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
Which utility for which data UI problem Matrix of problems — silent sort, flood of messages, focus ring on mouse click, dialog escape, lost focus after close — against LiveAnnouncer, a wrapper service, FocusMonitor, FocusTrap and Dialog restore. Which utility for which data UI problemLiveAnnouncerWrapper serviceFocusMonitorFocusTrap /DialogSilent sortYesMergingNoNoMessage floodPartialYesNoNoFocus ring on clickNoNoYesNoDialog escapeNoNoNoYesLost focus after closeNoNoOriginRestore
Most problems map to one utility, plus a thin service.

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"
  1. Provide a DataAnnouncer service wrapping LiveAnnouncer with keys and merging.
  2. Lint against aria-live attributes in component templates.
  3. Use CDK Dialog (or Material Dialog) for modal panels; never hand-rolled traps.
  4. Add .cdk-keyboard-focused styles to the design system, or rely on :focus-visible consistently.
  5. 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 LiveAnnouncer and route every data message through it, choosing politeness per message.
  • Use FocusMonitor to 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.

Permalink to "Related"

← Back to Angular Data Components