Angular Aria (@angular/aria, introduit en v21) est une collection de directives headless : comportement complet, accessibilité (rôles ARIA, clavier, focus) et zéro style. C’est la base idéale pour votre propre design system : vous écrivez le CSS, Angular s’occupe de la partie difficile — exactement ce que presque tout le monde rate quand il écrit des onglets ou des listes déroulantes à la main.
npm i @angular/aria
# Patterns: Accordion, Combobox (autocomplete/select), Grid, Listbox, Menu, Tabs, Toolbar, TreeOnglets
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">Sécurité</li>
</ul>
<div ngTabPanel value="profil"><ng-template ngTabContent>…</ng-template></div>
<div ngTabPanel value="securitate"><ng-template ngTabContent>…</ng-template></div>
</div>
`,
styles: `
/* Vous stylez via les attributs ARIA que posent les directives */
[ngTab][aria-selected='true'] { border-bottom: 2px solid var(--accent); }
`,
})
export class SettingsTabs {
readonly selected = signal<string | undefined>('profil');
}- Profil
- Sécurité
- Notifications
selectedTab (model): profil
Accordéon
<div ngAccordionGroup [multiExpandable]="false">
<button ngAccordionTrigger [panel]="p1">Question 1</button>
<div ngAccordionPanel #p1="ngAccordionPanel">
<ng-template ngAccordionContent>Réponse rendue en lazy, à l’ouverture.</ng-template>
</div>
</div>Listbox
<!-- selectionMode="explicit" : un clic / Espace bascule la sélection (sinon la sélection suit le focus) -->
<ul ngListbox [multi]="true" selectionMode="explicit" [(value)]="toppings" aria-label="Garnitures">
@for (t of options; track t) {
<li ngOption [value]="t" [label]="t">{{ t }}</li>
}
</ul>- ✓Mozzarella
- ✓Champignons
- ✓Olives
- ✓Poivrons
- ✓Ananas
value (model): mozzarella · clavier : ↑ ↓, Espace, Ctrl+A, tapez une lettre
Menu : un menu avec sous-menu
Un bouton avec ngMenuTrigger ouvre un ngMenu ; chaque option est un ngMenuItem, et un item avec [submenu] ouvre un autre menu. Vous obtenez le clavier complet (flèches, Entrée, Échap, typeahead) et les rôles menu / menuitem. Le choix arrive par l’output itemSelected, et avec ng-template ngMenuContent le contenu n’est rendu qu’à l’ouverture du menu.
<!-- le menu s’ouvre depuis le bouton ; « Partager » ouvre un sous-menu -->
<button ngMenuTrigger [menu]="actions">Actions</button>
<div ngMenu #actions="ngMenu" (itemSelected)="run($event)">
<ng-template ngMenuContent>
<div ngMenuItem value="edit">Modifier</div>
<div ngMenuItem value="duplicate">Dupliquer</div>
<div ngMenuItem value="share" [submenu]="share">Partager</div>
</ng-template>
</div>
<div ngMenu #share="ngMenu" (itemSelected)="run($event)">
<div ngMenuItem value="link">Copier le lien</div>
<div ngMenuItem value="email">Email</div>
</div>Toolbar : un seul arrêt de Tab pour un groupe de boutons
Dans une barre d’outils, Tab doit entrer une seule fois dans le groupe et les flèches déplacer le focus entre les boutons (le roving tabindex). ngToolbar fait exactement cela : vous marquez chaque bouton avec ngToolbarWidget, la directive gère tabindex, l’orientation et les boutons désactivés.
<!-- Tab entre une fois dans la barre ; ← → déplacent le focus entre les boutons -->
<div ngToolbar aria-label="Mise en forme">
<button ngToolbarWidget (click)="bold()">B</button>
<button ngToolbarWidget (click)="italic()">I</button>
<button ngToolbarWidget [disabled]="!canUndo()" (click)="undo()">↶</button>
</div>Le style : les attributs ARIA sont l’API
Les directives n’ajoutent pas de classes ; elles posent les bons attributs ARIA : aria-selected, aria-expanded, aria-disabled. Stylez d’après eux, pas d’après des classes posées à la main. Ainsi l’apparence ne peut pas prendre de retard sur l’état : si le lecteur d’écran dit « sélectionné », l’utilisateur voit aussi « sélectionné ».
/* L’état vient des attributs posés par les directives, pas de vos propres classes */
[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;
}Tester avec des harnesses
Chaque pattern a un harness dans @angular/aria/<pattern>/testing. Le test parle le langage de l’utilisateur (« sélectionne l’onglet Sécurité »), pas celui des sélecteurs CSS, il ne casse donc pas quand vous changez le markup.
import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
import { TabsHarness } from '@angular/aria/tabs/testing';
it('sélectionne l’onglet par son titre', async () => {
const fixture = TestBed.createComponent(SettingsPage);
const loader = TestbedHarnessEnvironment.loader(fixture);
const tabs = await loader.getHarness(TabsHarness);
await tabs.selectTab({ title: 'Sécurité' });
const selected = await tabs.getSelectedTab();
expect(await selected?.getTitle()).toBe('Sécurité');
});Aria, CDK ou Material ?
| Angular Aria | Angular CDK | Angular Material | |
|---|---|---|---|
| Ce que c’est | patterns d’interface headless accessibles | primitives de comportement (overlay, drag, a11y, défilement) | composants complets, avec le style Material |
| Style | aucun | aucun | Material Design (personnalisable avec des tokens) |
| Quand | votre propre design system | briques de bas niveau | vous voulez une interface prête, vite |
Un contrôle construit sur Aria se branche aux formulaires comme dans Contrôles de formulaire personnalisés ; le style via les attributs ARIA suit les règles de Styles des composants et encapsulation.
Quand OUI, quand NON
- Vous construisez vos propres composants (un design system) et voulez une accessibilité correcte dès le départ.
- Vous avez des onglets / menus / comboboxes écrits à la main, sans prise en charge du clavier.
- Vous avez besoin d’un contrôle total sur le markup et le style.
- Vous utilisez déjà Material / PrimeNG / une autre bibliothèque complète — le comportement y est inclus.
- Vous avez seulement besoin d’un overlay ou du glisser-déposer → CDK.
Pièges fréquents
- Liste ou groupe d’onglets sans nom : s’il n’y a pas de titre visible, ajoutez
aria-label. Sinon le lecteur d’écran annonce seulement « liste » ou « liste d’onglets ». - Des classes d’état posées à la main à côté de l’état Aria (
[class.active]) : au premier oubli elles ne correspondent plus. Stylez d’aprèsaria-*. - Votre propre
(click)sur les items : vous doublez le comportement et perdez le clavier. Écoutez l’output de la directive (itemSelected) ou liezvalue. - Un
<select>natif suffit parfois : il est accessible gratuitement et marche parfaitement sur mobile. Aria se justifie quand il vous faut une apparence ou un comportement que l’élément natif ne peut pas offrir.
Aide-mémoire
@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
harnesses de test : @angular/aria/<pattern>/testing (ex. TabsHarness, MenuHarness)