Angular Aria (@angular/aria, eingeführt in v21) ist eine Sammlung headless Direktiven: vollständiges Verhalten, Barrierefreiheit (ARIA-Rollen, Tastatur, Fokus) und null Styles. Sie ist die ideale Grundlage für dein eigenes Design System: Du schreibst das CSS, Angular übernimmt den schwierigen Teil — genau das, was fast alle falsch machen, wenn sie Tabs oder Dropdowns von Hand schreiben.
npm i @angular/aria
# Patterns: Accordion, Combobox (autocomplete/select), Grid, Listbox, Menu, Tabs, Toolbar, TreeTabs
import { Tab, TabContent, TabList, TabPanel, Tabs } from '@angular/aria/tabs';
@Component({
imports: [Tabs, TabList, Tab, TabPanel, TabContent],
template: `
<div ngTabs>
<ul ngTabList [(selectedTab)]="selected">
<li ngTab value="profil">Profil</li>
<li ngTab value="securitate">Sicherheit</li>
</ul>
<div ngTabPanel value="profil"><ng-template ngTabContent>…</ng-template></div>
<div ngTabPanel value="securitate"><ng-template ngTabContent>…</ng-template></div>
</div>
`,
styles: `
/* Du stylst über die ARIA-Attribute, die die Direktiven setzen */
[ngTab][aria-selected='true'] { border-bottom: 2px solid var(--accent); }
`,
})
export class SettingsTabs {
readonly selected = signal<string | undefined>('profil');
}- Profil
- Sicherheit
- Benachrichtigungen
selectedTab (model): profil
Accordion
<div ngAccordionGroup [multiExpandable]="false">
<button ngAccordionTrigger [panel]="p1">Frage 1</button>
<div ngAccordionPanel #p1="ngAccordionPanel">
<ng-template ngAccordionContent>Antwort, lazy beim Öffnen gerendert.</ng-template>
</div>
</div>Listbox
<!-- selectionMode="explicit": Klick / Leertaste schaltet die Auswahl um (sonst folgt die Auswahl dem Fokus) -->
<ul ngListbox [multi]="true" selectionMode="explicit" [(value)]="toppings" aria-label="Beläge">
@for (t of options; track t) {
<li ngOption [value]="t" [label]="t">{{ t }}</li>
}
</ul>- ✓Mozzarella
- ✓Champignons
- ✓Oliven
- ✓Paprika
- ✓Ananas
value (model): mozzarella · Tastatur: ↑ ↓, Leertaste, Strg+A, einen Buchstaben tippen
Menu: ein Menü mit Untermenü
Ein Button mit ngMenuTrigger öffnet ein ngMenu; jede Option ist ein ngMenuItem, und ein Item mit [submenu] öffnet ein weiteres Menü. Du bekommst die volle Tastatursteuerung (Pfeile, Enter, Escape, Typeahead) und die Rollen menu / menuitem. Die Auswahl kommt über den Output itemSelected, und mit ng-template ngMenuContent wird der Inhalt erst beim Öffnen gerendert.
<!-- das Menü öffnet sich über den Button; „Teilen“ öffnet ein Untermenü -->
<button ngMenuTrigger [menu]="actions">Aktionen</button>
<div ngMenu #actions="ngMenu" (itemSelected)="run($event)">
<ng-template ngMenuContent>
<div ngMenuItem value="edit">Bearbeiten</div>
<div ngMenuItem value="duplicate">Duplizieren</div>
<div ngMenuItem value="share" [submenu]="share">Teilen</div>
</ng-template>
</div>
<div ngMenu #share="ngMenu" (itemSelected)="run($event)">
<div ngMenuItem value="link">Link kopieren</div>
<div ngMenuItem value="email">Email</div>
</div>Toolbar: ein Tab-Stopp für eine Gruppe von Buttons
In einer Toolbar soll Tab die Gruppe einmal betreten, und die Pfeiltasten verschieben den Fokus zwischen den Buttons (das sogenannte Roving Tabindex). ngToolbar macht genau das: Du markierst jeden Button mit ngToolbarWidget, die Direktive verwaltet tabindex, Ausrichtung und deaktivierte Buttons.
<!-- Tab betritt die Leiste einmal; ← → verschieben den Fokus zwischen den Buttons -->
<div ngToolbar aria-label="Formatierung">
<button ngToolbarWidget (click)="bold()">B</button>
<button ngToolbarWidget (click)="italic()">I</button>
<button ngToolbarWidget [disabled]="!canUndo()" (click)="undo()">↶</button>
</div>Styling: ARIA-Attribute sind die API
Die Direktiven setzen keine Klassen, sondern die richtigen ARIA-Attribute: aria-selected, aria-expanded, aria-disabled. Style nach diesen, nicht nach selbst gesetzten Klassen. So kann das Aussehen dem Zustand nicht hinterherhinken: Sagt der Screenreader „ausgewählt“, sieht der Nutzer auch „ausgewählt“.
/* Der Zustand kommt aus den Attributen der Direktiven, nicht aus eigenen Klassen */
[ngTab][aria-selected='true'] {
border-bottom: 2px solid var(--accent);
}
[ngAccordionTrigger][aria-expanded='true'] .chevron {
rotate: 180deg;
}
[ngOption][aria-selected='true']::before {
content: '✓ ';
}
[aria-disabled='true'] {
opacity: 0.5;
cursor: not-allowed;
}Testen mit Harnesses
Jedes Pattern hat einen Harness in @angular/aria/<pattern>/testing. Der Test spricht in der Sprache des Nutzers („wähle den Tab Sicherheit“), nicht in CSS-Selektoren, und bricht deshalb nicht, wenn du das Markup änderst.
import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
import { TabsHarness } from '@angular/aria/tabs/testing';
it('wählt den Tab über seinen Titel', async () => {
const fixture = TestBed.createComponent(SettingsPage);
const loader = TestbedHarnessEnvironment.loader(fixture);
const tabs = await loader.getHarness(TabsHarness);
await tabs.selectTab({ title: 'Sicherheit' });
const selected = await tabs.getSelectedTab();
expect(await selected?.getTitle()).toBe('Sicherheit');
});Aria, CDK oder Material?
| Angular Aria | Angular CDK | Angular Material | |
|---|---|---|---|
| Was es ist | barrierefreie Headless-UI-Patterns | Verhaltens-Primitive (Overlay, Drag, A11y, Scrolling) | fertige Komponenten, mit Material-Styling |
| Styling | keins | keins | Material Design (anpassbar mit Tokens) |
| Wann | eigenes Design System | Low-Level-Bausteine | du willst schnell eine fertige UI |
Ein auf Aria gebautes Control bindest du wie in Eigene Formular-Controls an Formulare an; Styling über ARIA-Attribute folgt den Regeln aus Komponenten-Styles & Kapselung.
Wann ja, wann nein
- Du baust eigene Komponenten (ein Design System) und willst von Anfang an korrekte Barrierefreiheit.
- Du hast von Hand geschriebene Tabs / Menüs / Comboboxen ohne Tastaturunterstützung.
- Du brauchst volle Kontrolle über Markup und Styling.
- Du nutzt bereits Material / PrimeNG / eine andere vollständige Bibliothek — dort ist das Verhalten enthalten.
- Du brauchst nur ein Overlay oder Drag & Drop → CDK.
Häufige Stolperfallen
- Liste oder Tab-Gruppe ohne Namen: Gibt es keine sichtbare Überschrift, setze
aria-label. Sonst sagt der Screenreader nur „Liste“ oder „Tab-Liste“. - Zustandsklassen von Hand neben dem Aria-Zustand (
[class.active]): Beim ersten Versehen passen sie nicht mehr zusammen. Style nacharia-*. - Eigenes
(click)auf Items: Du verdoppelst das Verhalten und verlierst die Tastatur. Hör auf den Output der Direktive (itemSelected) oder bindevalue. - Ein natives
<select>reicht manchmal: Es ist gratis barrierefrei und funktioniert auf dem Handy perfekt. Aria lohnt sich, wenn du ein Aussehen oder Verhalten brauchst, das das native Element nicht bietet.
Spickzettel
@angular/aria/tabs ngTabs ngTabList ngTab ngTabPanel
@angular/aria/accordion ngAccordionGroup ngAccordionTrigger ngAccordionPanel
@angular/aria/listbox ngListbox ngOption
@angular/aria/menu ngMenuTrigger ngMenu ngMenuItem ngMenuBar ngMenuContent
@angular/aria/toolbar ngToolbar ngToolbarWidget ngToolbarWidgetGroup
@angular/aria/combobox ngCombobox ngComboboxWidget
@angular/aria/tree ngTree ngTreeItem
@angular/aria/grid ngGrid ngGridRow ngGridCell ngGridCellWidget
Test-Harnesses: @angular/aria/<pattern>/testing (z. B. TabsHarness, MenuHarness)