Week 22: Animations, Accessibility & Internationalization

Welcome to Phase 6 — the remaining weeks are less about new mechanics and more about polish: making an app that feels good to use, works for users with disabilities, and reaches people who don't read English.

Phase 6 of 7 Week 22 of 26 ~4 Hours Hands-on Exercise Included

By the end of this week, you'll be able to

  • Ship an app that's usable, accessible and translatable
  • Manage focus correctly for keyboard-only users
  • Run and act on a real accessibility audit

1. Angular Animations API & CSS-Driven Alternatives

Angular's animation package gives you programmatic control over enter/leave transitions triggered by state changes — useful when a transition needs to know about component state, not just a CSS class.

notification-toast.ts
import { Component, input } from '@angular/core';
import { trigger, transition, style, animate } from '@angular/animations';

@Component({
  selector: 'app-notification-toast',
  templateUrl: './notification-toast.html',
  animations: [
    trigger('slideIn', [
      transition(':enter', [
        style({ transform: 'translateY(-16px)', opacity: 0 }),
        animate('200ms ease-out', style({ transform: 'translateY(0)', opacity: 1 })),
      ]),
      transition(':leave', [
        animate('150ms ease-in', style({ transform: 'translateY(-16px)', opacity: 0 })),
      ]),
    ]),
  ],
})
export class NotificationToast {
  message = input.required<string>();
}

For simpler cases — a hover effect, a straightforward fade — plain CSS transitions often do the same job with less code and no extra bundle weight from the animations package at all:

card.css — CSS-only alternative
.card {
  transition: transform 0.2s ease, box-shadow 0.2s ease;
}

.card:hover {
  transform: translateY(-4px);
  box-shadow: var(--shadow-lg);
}

Reach for the animations API specifically when a transition needs to react to Angular-level state (a signal changing, an element actually entering/leaving the DOM via @if) — reach for plain CSS for anything driven purely by user interaction like :hover or :focus.

2. Reduced-Motion Support

Some users experience genuine discomfort or disorientation from motion — the prefers-reduced-motion media query lets you respect that operating-system level preference directly.

respecting the preference — CSS
@media (prefers-reduced-motion: reduce) {
  .card {
    transition: none;
  }
}
respecting the preference — in TypeScript
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;

if (!prefersReducedMotion) {
  // only run the toast slide-in animation, confetti, parallax scroll, etc.
}

This isn't a nice-to-have layered on top of accessibility — it's an accessibility requirement in its own right, and one of the easiest to actually implement correctly once you know it exists.

3. WCAG Basics & Accessible Component Patterns

WCAG (Web Content Accessibility Guidelines) organizes accessibility around four principles, worth knowing by name: content must be Perceivable, Operable, Understandable, and Robust. A few concrete, high-impact patterns that follow from them:

  • Every interactive element needs an accessible name — visible text, or aria-label when there's no visible text (an icon-only button).
  • Color alone should never be the only way information is conveyed — an error state needs an icon or text, not just red.
  • Custom components mimicking native ones (a custom dropdown, a custom checkbox) need the matching ARIA role and keyboard behavior — or you're better off starting from the real native element and styling it.
icon-only-button.html
<button aria-label="Delete item" (click)="delete()">
  <svg aria-hidden="true">...</svg>
</button>

aria-hidden="true" on the icon prevents assistive technology from announcing the SVG's raw contents redundantly — the button's aria-label already says everything that needs saying.

4. Keyboard Navigation & Focus Management

A keyboard-only user navigates entirely via Tab, Shift+Tab, Enter, and arrow keys — no mouse at all. You already used the CDK's cdkTrapFocus for this back in Week 7; this section is the broader pattern it's part of.

moving focus programmatically — modal.ts
import { Component, ElementRef, viewChild, effect } from '@angular/core';

export class Modal {
  private closeButton = viewChild<ElementRef<HTMLButtonElement>>('closeBtn');
  open = model(false);

  constructor() {
    effect(() => {
      if (this.open()) {
        // Move focus INTO the modal the moment it opens
        this.closeButton()?.nativeElement.focus();
      }
    });
  }
}

Two rules cover most focus-management needs: when a modal or menu opens, move focus into it (usually to the first focusable element or a close button); when it closes, move focus back to whatever element opened it. Without the second rule, a keyboard user's focus silently lands on <body> after closing a dialog, and they have to start tabbing from the top of the page again.

5. Angular i18n & Runtime Translation Strategies

Angular's built-in @angular/localize is a build-time approach — it produces a separate compiled build per locale, which is fast at runtime but means deploying multiple builds.

welcome.html — i18n attribute
<h1 i18n="@@welcomeHeading">Welcome back, {{ user().name }}</h1>
terminal
ng extract-i18n   # generates a translation source file (messages.xlf)
ng build --localize   # builds one output per configured locale

The alternative — a runtime translation library (like ngx-translate) — loads translation JSON dynamically and switches language without a rebuild or page reload, at the cost of a small runtime overhead and translations that aren't statically checked at build time. Build-time i18n fits an app with a small, known set of locales; runtime fits an app where users switch language on the fly, or locales are added without a full redeploy.

6. Auditing an App with Lighthouse/axe

Automated tools catch a meaningful subset of accessibility issues — not everything (real screen-reader testing and manual keyboard testing still matter), but enough to be worth running on every significant change.

terminal — axe via CLI
npx @axe-core/cli http://localhost:4200

Chrome DevTools' Lighthouse panel runs a broader audit — accessibility, performance (tying back to Week 20), best practices, and SEO — each producing a scored report with specific, actionable findings rather than a vague pass/fail.

A high score isn't the finish line

Automated tools catch missing labels, insufficient color contrast, and invalid ARIA usage — but they can't tell you whether your focus order is logical, or whether your custom dropdown is actually usable with a screen reader. A perfect Lighthouse score is a floor, not proof of genuine accessibility.

7. Hands-on Exercise

Hands-on

Audit your dashboard app and add a second language

Apply this week's tools directly to the app you've been building since Week 9.

Requirements:

  1. Run both the Lighthouse accessibility audit and axe against your dashboard app. Fix every issue reported — not just the highest-severity ones.
  2. Navigate your entire app using only the keyboard (unplug your mouse if you have to). Fix every place focus gets lost, trapped incorrectly, or lands somewhere illogical.
  3. Add prefers-reduced-motion handling to every animation or transition in the app.
  4. Pick one meaningful section of the app and translate it into a second language using @angular/localize — extract the strings, translate them, and build a locale-specific version.
  5. Re-run both audits after your fixes and confirm the score improved, with zero critical issues remaining.
Hint

If you get stuck navigating your own app by keyboard, that's the exercise working as intended — it's a genuinely common experience for developers auditing their own work for the first time.

8. Knowledge Check

Four quick questions. Expand each to check your answer.

Q1

When should you reach for Angular's animations API instead of a plain CSS transition?

When the transition needs to react to Angular-level state changes — an element actually entering or leaving the DOM via @if, or a signal-driven trigger — rather than pure CSS pseudo-classes like :hover or :focus, which plain CSS handles natively with no extra bundle cost.

Q2

Why is aria-hidden="true" added to the SVG icon inside an icon-only button that already has an aria-label?

Without it, a screen reader might attempt to describe the SVG's raw internal content (path data, or nothing meaningful) in addition to announcing the button's aria-label — redundant or confusing. Hiding the icon from assistive technology means only the deliberate, human-readable label gets announced.

Q3

When a modal closes, why does focus need to move back to the element that opened it, specifically?

Without an explicit destination, focus typically resets to <body> once the focused element (something inside the now-closed modal) is removed from the DOM. A keyboard-only user would then have to tab from the very top of the page to get back to where they were, instead of continuing naturally from the button they just used.

Q4

What's the core tradeoff between Angular's build-time i18n and a runtime translation library?

Build-time i18n produces a separate, fully-compiled build per locale — fast at runtime with no translation-loading overhead, but switching languages means loading a different build, and adding a locale requires a rebuild and redeploy. A runtime library loads translations dynamically, letting a user switch language instantly without reloading the app, at the cost of runtime overhead and translations that aren't checked at build time the way build-time i18n's are.