We use cookies to enhance your experience on the site
CodeWorlds

Accessibility (a11y)

Accessibility is like building a castle with gates for all guests:

Accessibility Basics

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}

Focus Management

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}

Keyboard Navigation

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}

Accessibility Testing

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
Go to CodeWorlds