Accessibility is like building a castle with gates for all guests:
1@Component({
2 template: \`
3 <!-- Semantic HTML -->
4 <nav aria-label="Main navigation">
5 <ul>
6 @for (link of navLinks; track link.path) {
7 <li>
8 <a [routerLink]="link.path"
9 [attr.aria-current]="isActive(link.path) ? 'page' : null">
10 {{ link.label }}
11 </a>
12 </li>
13 }
14 </ul>
15 </nav>
16
17 <!-- Accessible forms -->
18 <form>
19 <label for="samuraiName">Samurai name</label>
20 <input
21 id="samuraiName"
22 type="text"
23 [attr.aria-invalid]="nameControl.invalid"
24 [attr.aria-describedby]="nameControl.invalid ? 'nameError' : null"
25 >
26 @if (nameControl.invalid) {
27 <span id="nameError" role="alert">
28 Name is required
29 </span>
30 }
31 </form>
32
33 <!-- Buttons with descriptions -->
34 <button
35 (click)="deleteSamurai()"
36 [attr.aria-label]="'Delete samurai ' + samurai.name">
37 <mat-icon>delete</mat-icon>
38 </button>
39
40 <!-- Live regions for dynamic content -->
41 <div aria-live="polite" class="sr-only">
42 {{ statusMessage() }}
43 </div>
44 \`
45})
46export class AccessibleComponent {
47 // Screen reader only class
48 // .sr-only { position: absolute; width: 1px; height: 1px; ... }
49}1@Component({
2 template: \`
3 <dialog #dialog>
4 <h2>Dialog Title</h2>
5 <button #closeBtn (click)="close()">Close</button>
6 </dialog>
7 \`
8})
9export class DialogComponent implements AfterViewInit {
10 @ViewChild('dialog') dialog!: ElementRef<HTMLDialogElement>;
11 @ViewChild('closeBtn') closeBtn!: ElementRef<HTMLButtonElement>;
12
13 private previousFocus: Element | null = null;
14
15 open(): void {
16 this.previousFocus = document.activeElement;
17 this.dialog.nativeElement.showModal();
18 this.closeBtn.nativeElement.focus();
19 }
20
21 close(): void {
22 this.dialog.nativeElement.close();
23 (this.previousFocus as HTMLElement)?.focus();
24 }
25}1@Directive({
2 selector: '[appKeyboardNav]',
3 standalone: true
4})
5export class KeyboardNavDirective {
6 @HostListener('keydown', ['$event'])
7 onKeyDown(event: KeyboardEvent): void {
8 const items = this.getNavigableItems();
9 const currentIndex = items.indexOf(document.activeElement as HTMLElement);
10
11 switch (event.key) {
12 case 'ArrowDown':
13 event.preventDefault();
14 items[(currentIndex + 1) % items.length]?.focus();
15 break;
16 case 'ArrowUp':
17 event.preventDefault();
18 items[(currentIndex - 1 + items.length) % items.length]?.focus();
19 break;
20 case 'Home':
21 event.preventDefault();
22 items[0]?.focus();
23 break;
24 case 'End':
25 event.preventDefault();
26 items[items.length - 1]?.focus();
27 break;
28 }
29 }
30
31 private getNavigableItems(): HTMLElement[] {
32 return Array.from(
33 this.el.nativeElement.querySelectorAll('[tabindex="0"], button, a')
34 );
35 }
36}1# Lighthouse audit
2npm install -g lighthouse
3lighthouse http://localhost:4200 --only-categories=accessibility
4
5# axe-core in tests
6npm install axe-core @axe-core/playwright