Angular Signals and Live Announcements

Permalink to "Angular Signals and Live Announcements"

Angular’s signals make data state explicit: filters, sort, page and results become signals, and derived values — like “42 invoices, sorted by Amount” — become computed signals. That is a natural fit for announcements: the message is a pure function of state. The care needed is in when to announce. An effect that announces every time the computed message changes will also fire on initial render, after hydration, and on programmatic state restores — all moments when users did not ask for anything.

This guide belongs to Angular data components.

Spec reference

Permalink to "Spec reference"
  • SC 4.1.3 Status Messages (AA) — status messages for results of user actions.
  • Signals API — signal(), computed(), effect(); untracked() to read without subscribing.
  • LiveAnnouncer (CDK) — the output channel.
  • Timing principle: announce changes users caused; stay silent for initial and restored state.
From signals to a spoken message Chain: user changes a filter signal; results recompute; a computed message updates; an effect sees the change, checks the user-initiated flag, debounces and calls LiveAnnouncer. From signals to a spoken messageFilter signaluser types "overdue"Resultsfiltered rows: 42Computed message"42 invoices, sortedby Amount"Gateuser-initiated?debounce 400 msrecomputederiveeffectLiveAnnouncerpolite message
State drives text; intent decides whether to speak.

When to use — and when not to

Permalink to "When to use — and when not to"

Use computed messages for any announcement that summarises state: result counts, sort order, page position, selection counts. Use effects with an intent gate to announce them.

Do not announce from effects without a gate; you will announce on load. Do not put the announcement text in the template’s visible DOM and also announce it, unless that visible text is the status region itself.

The misapplication: effect(() => announcer.announce(message())) with no gate, which speaks “240 invoices” every time the page loads and every time a websocket refresh updates data.

Ungated effect versus gated effect Comparison: an ungated effect announces on load, on refresh and on restore; a gated effect announces only after user actions, merged and debounced. Ungated effect versus gated effect✗ Ungated effectAnnounces on first renderAnnounces after hydrationAnnounces background refreshesEvery intermediate valueDuplicated with template regions✓ Gated effectSilent on first renderSilent after hydrationSilent unless importantDebounced final valueOne channel: LiveAnnouncer

Annotated code example

Permalink to "Annotated code example"
@Component({ selector: 'app-invoices', templateUrl: './invoices.html' })
export class InvoicesComponent {
  private announcer = inject(LiveAnnouncer);
  filter = signal('');
  sort = signal<{ field: string; dir: 'asc' | 'desc' } | null>(null);
  rows = input.required<Invoice[]>();

  visible = computed(() => sortRows(filterRows(this.rows(), this.filter()), this.sort()));
  message = computed(() => {                                    // pure text from state
    const s = this.sort();
    return `${this.visible().length} invoices` + (s ? `, sorted by ${label(s.field)} ${s.dir === 'asc' ? 'ascending' : 'descending'}` : '');
  });

  private userChanged = false;                                  // intent gate
  private timer?: ReturnType<typeof setTimeout>;

  constructor() {
    effect(() => {
      const text = this.message();                              // track message changes
      if (!this.userChanged) return;                            // silent on load and restores (SC 4.1.3 intent)
      clearTimeout(this.timer);
      this.timer = setTimeout(() => this.announcer.announce(text, 'polite'), 400);   // debounce
    });
  }

  onFilterInput(v: string) { this.userChanged = true; this.filter.set(v); }
  onSort(field: string) { this.userChanged = true; this.sort.update(s => toggle(s, field)); }
}

Keyboard & AT behaviour

Permalink to "Keyboard & AT behaviour"
Event Signal changes Announced
Page load rows, message Nothing
Typing “overdue” (6 keystrokes) filter ×6 “42 invoices” once
Sort by Amount sort “42 invoices, sorted by Amount ascending”
Background refresh (same filter) rows Nothing (or a stale/refresh message if important)
Restore from URL on Back filter, sort Nothing
Change sources and announcement policy Matrix of change sources — initial load, hydration, user input, URL restore, background refresh, error — against whether to announce and how. Change sources and announcement policyAnnounce?HowInitial loadNopage heading carries contextHydrationNostate already visibleUser inputYesdebounced, politeURL restore (Back)Nofocus restore insteadBackground refreshIf importantmerged summaryErrorYesassertive, immediate
The source decides; the computed message only supplies text.

Integration context

Permalink to "Integration context"

The announcer itself is described in Angular CDK a11y: LiveAnnouncer and FocusMonitor. Timing rules mirror debouncing status messages for bulk operations and announcing filter result counts with aria-live. Live data refresh policies are covered in real-time data stream announcements.

Gotchas

Permalink to "Gotchas"

Effects run at least once. Every effect runs on creation; the gate must handle that first run.

Zoneless change detection. In zoneless Angular, effects still schedule correctly; avoid relying on zone-based timing for debouncing.

Server rendering. Effects that touch LiveAnnouncer should not run on the server; guard with isPlatformBrowser or use afterNextRender.

Common mistakes

Permalink to "Common mistakes"
  • Ungated effects that announce on load.
  • Announcing every intermediate filter value.
  • Treating background refreshes like user actions.
  • Duplicating messages in template live regions.

Adoption checklist for teams

Permalink to "Adoption checklist for teams"
  1. Model filter, sort, page and results as signals.
  2. Derive messages with computed signals in one place per component.
  3. Use a shared helper (e.g. announceOnUserChange(message, flag)) that implements gating and debouncing.
  4. Guard browser-only code for SSR.
  5. Unit test that loading does not announce and user actions do.

Version and upgrade notes

Permalink to "Version and upgrade notes"

Signals stabilised across recent Angular versions (signal inputs, effect behaviour changes). When upgrading, re-check that effects still run after the changes you expect, and that untracked usage is still correct.

Worked example

Permalink to "Worked example"

An Angular analytics page migrated to signals and added effect(() => announcer.announce(message())). Screen reader users started hearing “1,240 sessions” on every page load and every 30-second refresh. Adding a user-intent flag, a 400 ms debounce and a separate, rare message for background refreshes (“Data refreshed at 10:42”) restored calm. Tests now assert that loading the page produces no announcement.

A reusable helper

Permalink to "A reusable helper"
export function announceOnUserChange(message: Signal<string>, announcer: LiveAnnouncer, delay = 400) {
  let user = false, t: ReturnType<typeof setTimeout> | undefined;
  effect(() => { const m = message(); if (!user) return; clearTimeout(t); t = setTimeout(() => announcer.announce(m), delay); });
  return { markUserChange: () => (user = true) };
}

Components call markUserChange() in their event handlers. The helper keeps the gate and debounce identical across components, which keeps announcement timing consistent for users.

Testing checklist

Permalink to "Testing checklist"

Design system notes

Permalink to "Design system notes"

Provide a signals-based announcement helper in the design system’s Angular package, with the gate, debounce and SSR guard built in, so components only provide the computed message.

FAQ

Permalink to "FAQ"
Can I use computed signals for the status region text directly?

Yes, if the status region is in the template and always rendered; binding its text to a computed message works. Still gate changes on load if needed.

How long should the debounce be?

300–500 ms for typing; shorter for discrete actions like sorting.

How do I tell user changes from programmatic ones?

Set a flag in event handlers, or pass a source with each state update in a store.

Should errors be debounced?

No. Announce errors immediately and assertively.

Do signals change anything about accessibility itself?

No — they make state and derived text easier to manage. The timing and politeness rules stay the same.

How do I test announcements with signals?

Spy on LiveAnnouncer.announce, trigger user handlers, advance timers and assert calls; assert no call after initial render.

What about RxJS-based components?

The same gate and debounce apply; use debounceTime and filter on a user-action stream.

Should computed messages include the filter text?

Including it helps when users may have forgotten what they typed: “42 invoices match ‘overdue’”. Keep it short.

What about zoneless applications?

The pattern is the same. Effects schedule independently of zone.js, and setTimeout-based debouncing works without zones.

Permalink to "Related"

← Back to Angular Data Components