Accessibility (often written a11y) means people can use your app with a keyboard, a screen reader, zoom or other tools. It also improves usability and SEO for everyone. Most of it comes from good HTML, and Angular does not get in the way.
What you will learn
- Why semantic HTML comes first
- How to label forms and announce changes
- How to manage focus after navigation
1. Use the right element
<!-- Bad: a div is not focusable and has no role -->
<div class="btn" (click)="save()">Save</div>
<!-- Good: a button works with keyboard, screen readers and Enter/Space -->
<button type="button" (click)="save()">Save</button>
Use button for actions, a (with routerLink) for navigation, ul/li for lists, and nav, main, header, footer for page areas. A single h1 per page and ordered headings help screen reader users move around.
2. Label every input
<label for="email">Email</label>
<input id="email" type="email" autocomplete="email" />
A placeholder is not a label. Tie each label to its field with matching for and id, and show errors in text, not only in colour.
3. Announce changes
When something changes without a page load, such as a form error or "Saved", tell assistive technology:
// src/app/a11y/status-demo.ts
import { Component, signal } from '@angular/core';
@Component({
selector: 'app-status-demo',
template: `
<button type="button" (click)="save()">Save</button>
<p role="status" aria-live="polite">{{ message() }}</p>
@if (failed()) {
<p role="alert">Could not save. Please try again.</p>
}
`,
})
export class StatusDemo {
protected readonly message = signal('');
protected readonly failed = signal(false);
protected save(): void {
this.failed.set(false);
this.message.set('Saved successfully.');
}
}
role="status" (polite) is for messages that can wait. role="alert" interrupts, so keep it for real errors.
4. Move focus after navigation
With a single-page app the browser does not reload, so a screen reader may not notice the new page. Move focus to the main heading when the route changes:
// src/app/a11y/focus-on-navigation.ts
import { DestroyRef, inject } from '@angular/core';
import { NavigationEnd, Router } from '@angular/router';
import { filter } from 'rxjs';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
export function focusMainOnNavigation(): void {
const router = inject(Router);
const destroyRef = inject(DestroyRef);
router.events
.pipe(
filter((event) => event instanceof NavigationEnd),
takeUntilDestroyed(destroyRef),
)
.subscribe(() => {
const main = document.querySelector<HTMLElement>('main');
main?.setAttribute('tabindex', '-1');
main?.focus({ preventScroll: false });
});
}
// src/app/app.ts
import { Component } from '@angular/core';
import { RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router';
import { focusMainOnNavigation } from './a11y/focus-on-navigation';
@Component({
imports: [RouterOutlet, RouterLink, RouterLinkActive],
selector: 'app-root',
templateUrl: './app.html',
styleUrl: './app.css',
})
export class App {
constructor() {
focusMainOnNavigation();
}
}
<!-- src/app/app.html -->
<a class="skip-link" href="#content">Skip to main content</a>
<header>
<nav aria-label="Main">
<a routerLink="/" routerLinkActive="active" [routerLinkActiveOptions]="{ exact: true }">Home</a>
<a routerLink="/products" routerLinkActive="active">Products</a>
<a routerLink="/about" routerLinkActive="active">About</a>
</nav>
</header>
<main id="content">
<router-outlet />
</main>
/* src/app/app.css */
nav { display: flex; gap: 1rem; padding: 1rem 0; }
nav a.active { font-weight: bold; text-decoration: underline; }
.skip-link { position: absolute; left: -999px; }
.skip-link:focus { left: 1rem; top: 1rem; background: #fff; padding: .5rem; z-index: 10; }
main:focus { outline: none; }
5. Quick checks
- Use only the keyboard: can you reach and use every control, and always see where focus is?
- Check colour contrast (4.5:1 for normal text).
- Add
alttext to meaningful images andalt=""to decorative ones. - Run Lighthouse in Chrome DevTools and read the Accessibility report.
- The Angular CDK has tools such as
A11yModuleand focus trapping for dialogs.
Try it yourself
- Tab through your app and fix anything you cannot reach.
- Replace any clickable
divwith abutton. - Run Lighthouse and fix the first three accessibility issues.
Quick check
- Why is a
buttonbetter than a clickablediv? - What is the difference between
role="status"androle="alert"? - Why move focus after navigation in a single-page app?
Next, learn how to make the finished app fast and ready for production. The official accessibility guide goes deeper.