Angular sluit aan bij de headless UI-beweging

Met @angular/aria krijgt ook Angular een first-party tegenhanger van React Aria en Radix Primitives. Wat betekent dat voor het bredere headless UI-landschap?

Lange tijd was “headless UI” een React-feest waar Vue en Svelte op afstand naar keken, en waar Angular nauwelijks aan tafel zat. Met de komst van het @angular/aria-pakket is daar verandering in gekomen: Angular heeft nu zijn eigen first-party set ARIA-primitieven, vergelijkbaar met wat React Aria en Radix in de React-wereld doen. Een goed moment om te kijken hoe het landschap er nu uitziet — en wat die verschuiving in de praktijk betekent.

Wat is een UI-primitief eigenlijk?

De term komt overgewaaid uit de React-wereld, waar de afgelopen jaren een herkenbaar patroon is ontstaan: geen kant-en-klare componenten met opinies over styling, maar kale bouwstenen die alleen het gedrag dekken. Denk aan navigeren met het toetsenbord, het bijhouden van waar de focus zit, de juiste rollen en states voor screen readers. Hoe het eruit ziet, is volledig aan jou.

Het idee is dat toegankelijkheid en interactiegedrag verrassend uniform zijn over apps heen — een accordion is een accordion, een listbox is een listbox — maar dat de styling dat juist nooit is. Door die twee lagen te scheiden krijg je een herbruikbare basis zonder dat je je hele designsysteem aan iemand anders’ esthetiek vastklinkt.

Een korte oriëntatie

Voordat we naar Angular kijken, even het speelveld. Headless primitieven zijn de afgelopen vijf jaar in vrijwel elk frontend-ecosysteem opgedoken.

React Aria (Adobe, sinds 2020) is de oudste en meest doorgewinterde van het stel. Het bestaat uit hooks die ARIA-rollen, toetsenbordgedrag en locale-aware interactie leveren, en vormt onder water ook de basis van React Spectrum, Adobe’s eigen designsysteem.

Radix Primitives (sinds 2021) gaf de stijl een naam: gecomponeerde React-componenten zonder styling, met een API die door de shadcn/ui-beweging breed bekend werd. Headless UI van Tailwind Labs dekt een kleiner oppervlak, maar wordt veel gebruikt naast Tailwind zelf en bestaat zowel voor React als Vue.

In andere frameworks zijn vergelijkbare projecten ontstaan: Ark UI voor React, Vue en Solid (van de Chakra-makers), Reka UI als Vue-tegenhanger van Radix, en Melt UI voor Svelte.

In de Angular-wereld bleef het opvallend stil. Er waren community-pogingen — Spartan/ui was de bekendste, als shadcn-achtige port — maar geen daarvan groeide uit tot een breed gebruikte standaard. De Angular CDK bood al jaren een a11y-module met laag-niveau gereedschap (FocusTrap, ListKeyManager, LiveAnnouncer), maar geen volledige primitieven. Een serieuze tegenhanger van Radix of React Aria ontbrak feitelijk.

Het @angular/aria-pakket

@angular/aria is het Angular-antwoord op die leemte: een verzameling bouwstenen zonder eigen styling, die hun ARIA-state niet meer in de template laten leven, maar er een eersteklas reactieve waarde van maken die je vanuit een signal aanstuurt.

Het pakket dekt drie groepen:

  • Zoek en selectie — autocomplete, listbox, select, multiselect, combobox
  • Navigatie en acties — menu, menubar, toolbar
  • Contentorganisatie — accordion, tabs, tree, grid

Elke primitief levert geen kant-en-klare component op, maar een set directives. Die koppelen de juiste ARIA-attributen en het toetsenbordgedrag aan je eigen elementen, en stellen de bijbehorende state als signal beschikbaar — die lees je via een template-referentie uit, bijvoorbeeld item.expanded().

Een eenvoudig voorbeeld met een accordion:

import { Component } from '@angular/core';
import { Accordion, AccordionItem, AccordionTrigger, AccordionPanel }
  from '@angular/aria/accordion';

@Component({
  selector: 'app-faq',
  imports: [Accordion, AccordionItem, AccordionTrigger, AccordionPanel],
  template: `
    <div ngAccordion>
      <div ngAccordionItem #item="ngAccordionItem">
        <button ngAccordionTrigger>Werkt dit ook met screen readers?</button>
        <div ngAccordionPanel [hidden]="!item.expanded()">
          Ja — de juiste ARIA-attributen worden door de directives geleverd.
        </div>
      </div>
    </div>
  `,
})
export class Faq {}

Wat hier verandert ten opzichte van het oude patroon, is dat aria-expanded op de trigger en de relatie tussen trigger en panel niet langer iets zijn dat je in de template uitschrijft. De directives houden het bij, en de expanded()-signal op de item-referentie is de bron waar je zelf bindingen aan kunt knopen.

Verander je die state — bijvoorbeeld vanuit een query-param of een store — dan blijft alles in sync zonder dat je daar handmatig attributen voor hoeft bij te werken.

Wat dit oplost

De kern is simpel: toegankelijkheid zit nu in het framework, en niet meer in elke component opnieuw. Je hoeft het wiel niet uit te vinden voor dingen waar de specs allang antwoord op geven — welke rollen, welke states, welk toetsenbordgedrag een listbox of een menu hoort te hebben. Die kennis hoort niet in je featurecode te leven; die hoort in een primitief te zitten, en daar zit het nu ook.

Dat verandert in de praktijk drie dingen.

Het scheelt werk: niemand hoeft bij elke nieuwe combobox opnieuw uit te zoeken hoe Home, End, pijltjestoetsen en type-ahead zich horen te gedragen. Het scheelt bugs: ARIA-state en gedrag worden niet meer ergens halverwege je component vergeten, omdat ze niet meer iets zijn dat je zelf bij moet houden. En het maakt toegankelijkheid een standaard, geen extra ronde die team voor team opnieuw uitgevochten wordt — wie de primitief gebruikt, krijgt het correcte gedrag erbij.

Dat is in essentie wat React Aria en Radix in hun ecosystemen ook al een paar jaar doen, en wat Angular-teams tot nu toe zelf moesten oplossen.

De grotere beweging

Dat Angular deze stap zet is op zich niet schokkend — React Aria bestaat al jaren — maar het is wel een signaal dat het denken over componenten zelf verschuift. Niet langer “een Button is een component”, maar “een Button is een ARIA-rol plus state plus gedrag, waar je een element in jouw stijl aan koppelt”.

Dat ligt dichter bij hoe het platform zelf werkt: de browser kent ook geen “card-component”, hij kent rollen en states, en je componeert daar zelf iets visueels overheen.

Voor teams die hun designsysteem opnieuw bekijken is dit het moment om die scheiding bewust te trekken. ARIA is geen attribuut dat je op het laatst toevoegt. Het is, als het goed is, de eerste keuze die je maakt over hoe een interactie zich gedraagt.