Screen Reader Announcements with LiveAnnouncer

Among the utilities in the @angular/cdk/a11y package, the LiveAnnouncer service stands out. It lets you push messages to screen readers, which comes in handy whenever you need to give feedback after a user action. The beauty is in its simplicity — in many cases, I find myself swapping out console.log calls for announcements.

Here's what that looks like:

private readonly liveAnnouncer = inject(LiveAnnouncer);
if (this.flights().length > 0) {
  // console.log('Found ' + this.flights().length + ' flights');
  this.liveAnnouncer.announce('Found ' + this.flights().length + ' flights');
} else {
  // console.log('No flights found');
  this.liveAnnouncer.announce('No flights found');
}

Don't forget to verify your announcements with an actual screen reader.

Managing Focus

The CDK ships with a handful of utilities designed to help you keep keyboard focus exactly where it should be.

Focus Trap

With the FocusTrap Directive, you can confine keyboard navigation to a single element until the user chooses to leave it. It's an essential tool for modal dialogs or pop-ups where focus shouldn't wander:

<dialog class="awesome-dialog" cdkTrapFocus>
  <!–- Yay, focus won't leave this element! -->
</dialog>

For automatic focus entry, there's the cdkTrapFocusAutoCapture flag:

<dialog class="awesome-dialog" cdkTrapFocus [cdkTrapFocusAutoCapture]="true">
  <!–- Yay, focus won't leave this element! -->
</dialog>

With this flag on, focus is taken automatically as soon as the dialog appears. But an automatic focus grab isn't always the right UX, and there's a clever way around it:

<dialog class="awesome-dialog" cdkTrapFocus [cdkTrapFocusAutoCapture]="true">
  <h3 class="awesome-dialog__title" tabindex="-1" cdkFocusInitial>...</h3>
  <!–- Yay, focus won't leave this element! -->
</dialog>

Placing cdkFocusInitial on the title focuses it quietly at first. Since the title carries a tabindex of -1, it never shows up in normal keyboard navigation afterwards.

Focus Regions

With Regions, you can declare a section of the page where all focus activity should be contained. For intricate components like dropdowns or menus, this is the right approach when you want to keep focus within the component while it is open.

  • cdkFocusRegionStart
  • cdkFocusRegionEnd plus, optionally,
  • cdkFocusInitial

An example of Regions in action:

<nav>
  <a routerLink routerLinkActive="awesome" ariaCurrentWhenActive="page" cdkFocusRegionStart>Focus region start</a></li>
  <a routerLink routerLinkActive="awesome" ariaCurrentWhenActive="page">Another focusable link</a></li>
  <a routerLink routerLinkActive="awesome" ariaCurrentWhenActive="page" cdkFocusInitial>Initially focused</a></li>
  <a routerLink routerLinkActive="awesome" ariaCurrentWhenActive="page" cdkFocusRegionEnd>Focus region end</a></li>
</nav>

Note: You'll find my explanation of ariaCurrentWhenActive in the post about Accessible Angular Routes.

Focus Monitor

The FocusMonitor service hooks you into focus-related events anywhere in your app. It reports when an element becomes focused or loses it — which makes it a fine debugging instrument for focus problems.

Inject the service into your components (or other services) and subscribe to focus changes like this:

import { Component, DestroyRef, effect, ElementRef, inject, viewChild } from '@angular/core';
import { FocusMonitor, FocusOrigin } from '@angular/cdk/a11y';

@Component({
  selector: 'app-navbar',
  template: `<nav #observed class="awesome-nav-cnt"><!-- children --></nav>`,
})
export class AwesomeFocusMonitorComponent {
  private readonly destroyRef = inject(DestroyRef);
  private readonly focusMonitor = inject(FocusMonitor);
  private readonly observedElementRef = viewChild.required<ElementRef<HTMLElement>>('observed');

  constructor() {
    effect(() => {
      const observedElementRef = this.observedElementRef(); // effect will run when the view is initialized
      this.focusMonitor.monitor(observedElementRef, true).subscribe((origin: FocusOrigin) => console.log(origin));
      this.destroyRef.onDestroy(() => this.focusMonitor.stopMonitoring(observedElementRef));
    });
  }
}

Because it reveals exactly when an element takes or drops focus, this service is a valuable ally when tracking down focus-related bugs.

FocusOrigin takes one of these values:

  • 'mouse' when the pointer caused the focus
  • 'keyboard' when the keyboard did
  • 'touch' when a touchscreen did
  • 'program' for programmatic focus
  • null when the element has been blurred

So it can double as a way to detect a touch-enabled device 😏

Styling Helpers

Two Sass mixins complete the accessibility toolbox in the Angular A11y package.

Hidden Elements

Assistive technology — screen readers above all — will skip anything set to display: none, visibility: hidden, opacity: 0, height: 0, or width: 0. There are times, though, when an element should disappear from the screen yet stay readable for assistive technology (like screen readers).

@use '@angular/cdk';

@include cdk.a11y-visually-hidden();
<div class="awesome-toggle">
  <input type="checkbox" class="cdk-visually-hidden" />
</div>

High Contrast Mode

Many operating systems offer a High Contrast Mode. A Sass mixin from the Angular A11y package lets you add styles that are only applied when this mode is active. Wrap those styles in the high-contrast mixin:

@use '@angular/cdk';

button {
  @include cdk.high-contrast {
    outline: 3px solid gold;
  }
}

The mixin achieves this by aiming at the forced-colors media query.

Take a Workshop

If you'd like to go deeper into Angular, we have workshops in both English and German waiting for you:

Summing Up

These tools go a long way toward making your Angular Apps more inclusive. They keep your components functional, but also welcoming to every user, regardless of ability.

A final link: Google maintains an Angular CDK A11y codelab with hands-on exercises — https://codelabs.developers.google.com/angular-a11y#8

That's all folks!

That wraps up this A11y series. I hope you've picked up something useful about making your Angular Apps friendly for everyone, and that you're now prepared for the European Accessibility Act (EAA). If you need a refresher, just go back to the beginning of the A11y blog series.

This post was written by Alexander Thalhammer. You can follow him on Linkedin, X or github.

Further Reading