Angular Aria: el nuevo estándar para construir componentes accesibles
Kevin Dávila
Durante años, la accesibilidad en aplicaciones web fue tratada como un "nice to have". Algo que se agregaba al final, si acaso. El resultado: millones de usuarios con discapacidades visuales, motoras o cognitivas navegando apps que simplemente no funcionan para ellos.
Peeeero, con el paquete @angular/aria, el equipo de Angular propone un camino concreto para construir componentes accesibles desde el principio, sin sacrificar control sobre el HTML ni el diseño.
Primero, ¿qué es ARIA y por qué debería importarte?
ARIA son las siglas de Accessible Rich Internet Applications. Es una especificación del W3C que define un conjunto de atributos HTML que le comunican a los lectores de pantalla (como NVDA, JAWS o VoiceOver) cómo funciona un componente.
El problema es que los navegadores entienden natividad: saben que un <button> es clickeable, que un <input> es editable, que un <select> tiene opciones. Pero cuando construyes un componente personalizado, como un dropdown con <div>, el navegador no sabe nada. Para el lector de pantalla, es solo un bloque de texto invisible.
ARIA llena ese vacío. Le dice al lector de pantalla: "este div es un menú, este span es una opción seleccionable, este contenedor está expandido".
<!-- Sin ARIA: el lector de pantalla no entiende nada -->
<div class="dropdown">
<div class="option">Opción 1</div>
<div class="option">Opción 2</div>
</div>
<!-- Con ARIA: ahora sí tiene sentido -->
<div role="listbox" aria-label="Selecciona una opción">
<div role="option" aria-selected="true">Opción 1</div>
<div role="option" aria-selected="false">Opción 2</div>
</div>
El problema es que hacerlo bien a mano es difícil. La especificación WAI-ARIA tiene cientos de reglas. Los atributos deben sincronizarse con el estado. El teclado debe funcionar de formas específicas según el patrón. Si te equivocas, la experiencia para el usuario es peor que si no hubieras hecho nada.
Ahí entra @angular/aria.
Qué es @angular/aria
@angular/aria es una librería oficial de Angular que provee primitivas de accesibilidad headless: un conjunto de directivas que implementan los patrones de interacción definidos por la especificación WAI-ARIA.
La palabra clave es headless. La librería no te da componentes visuales. No hay estilos, no hay HTML predefinido. Te da comportamiento y semántica. Tú controlas completamente el markup y el CSS.
Esto es diferente a Angular Material. Material te da componentes listos para usar con accesibilidad incluida. @angular/aria es para cuando necesitas construir tu propio sistema de diseño, con tus propios componentes, pero sin reinventar la rueda en cada patrón de interacción.
Instalación
npm install @angular/aria
A partir de Angular 21, viene con soporte para 12 patrones de interacción.
Los 12 patrones disponibles
Angular 21 incluye directivas para los siguientes patrones:
Búsqueda y navegación:
Autocomplete
Combobox
Listbox
Menubar
Select
Multiselect
Menu
Toolbar
Organización de contenido:
Accordion
Tabs
Grid
Tree
Cada uno de estos implementa el comportamiento de teclado, los atributos ARIA y la gestión de foco que la especificación WAI-ARIA requiere para ese patrón específico.
Cómo funciona
Un combobox es ese campo de texto que abre una lista de sugerencias mientras escribes. Parece sencillo. En la práctica, tiene muchas reglas:
El campo debe tener role="combobox" y aria-expanded
La lista de opciones debe tener role="listbox"
Cada opción debe tener role="option" y aria-selected
El teclado debe manejar: flechas arriba/abajo, Enter para seleccionar, Escape para cerrar
El foco debe gestionarse correctamente cuando la lista se abre y cierra
Sin @angular/aria, implementas todo eso tú. Con @angular/aria:
// search.component.ts
import { Component, signal } from '@angular/core';
import { Combobox, ComboboxInput, ComboboxPopupContainer } from '@angular/aria/combobox';
import { Listbox, Option } from '@angular/aria/listbox';
@Component({
selector: 'app-search',
standalone: true,
imports: [Combobox, ComboboxInput, ComboboxPopupContainer, Listbox, Option],
template: `
<div ngCombobox>
<label id="search-label">Buscar ciudad</label>
<input
ngComboboxInput
aria-labelledby="search-label"
placeholder="Escribe para buscar..."
(input)="onSearch($event)"
/>
@if (resultados().length > 0) {
<div ngComboboxPopupContainer>
<ul ngListbox aria-labelledby="search-label">
@for (ciudad of resultados(); track ciudad.id) {
<li ngOption [value]="ciudad">{{ ciudad.nombre }}</li>
}
</ul>
</div>
}
</div>
`,
})
export class SearchComponent {
resultados = signal<Ciudad[]>([]);
onSearch(event: Event) {
const query = (event.target as HTMLInputElement).value;
this.resultados.set(this.buscarCiudades(query));
}
private buscarCiudades(query: string): Ciudad[] {
// lógica de búsqueda
return ciudades.filter(c =>
c.nombre.toLowerCase().includes(query.toLowerCase())
);
}
}
El HTML que renderiza este componente ya tiene todos los atributos ARIA correctos aplicados automáticamente. No escribiste role, no escribiste aria-expanded, no escribiste el handler de teclado para las flechas. Las directivas lo hacen por ti.
Un ejemplo más: Tabs
Las tabs son otro patrón que se ve sencillo y no lo es. La especificación define que los tabs deben responder a flechas izquierda/derecha, que el panel activo debe tener aria-labelledby apuntando al tab correspondiente, y más.
// tabs.component.ts
import { Component, signal } from '@angular/core';
import { TabGroup, Tab, TabPanel } from '@angular/aria/tabs';
@Component({
selector: 'app-info-tabs',
standalone: true,
imports: [TabGroup, Tab, TabPanel],
template: `
<div ngTabGroup>
<div role="tablist">
<button ngTab value="descripcion">Descripción</button>
<button ngTab value="especificaciones">Especificaciones</button>
<button ngTab value="resenas">Reseñas</button>
</div>
<div ngTabPanel value="descripcion">
<p>Descripción del producto...</p>
</div>
<div ngTabPanel value="especificaciones">
<p>Ficha técnica...</p>
</div>
<div ngTabPanel value="resenas">
<p>Opiniones de usuarios...</p>
</div>
</div>
`,
})
export class InfoTabsComponent {}
El ngTabGroup coordina el estado. ngTab y ngTabPanel se registran automáticamente. La navegación con teclado, el aria-controls, el aria-selected, el role="tab" y role="tabpanel" — todo gestionado.
Lo que las directivas hacen por ti (y lo que no tendrías que hacer tú)
Cada directiva de @angular/aria se encarga de:
Atributos ARIA Aplica y mantiene sincronizados los roles y estados necesarios: role, aria-expanded, aria-selected, aria-controls, aria-labelledby, etc. Cuando el estado cambia, los atributos cambian con él.
Navegación por teclado Cada patrón tiene reglas de teclado específicas según WAI-ARIA. Un menú usa flechas arriba/abajo. Un tablist usa flechas izquierda/derecha. Una grilla usa flechas en todas direcciones. Las directivas implementan esto correctamente.
Gestión de foco Cuando abres un modal, el foco debe ir al primer elemento interactivo dentro. Cuando lo cierras, debe volver al elemento que lo abrió. Manejar esto a mano es propenso a errores. Las directivas lo hacen bien.
Sincronización de estado Si la opción seleccionada cambia, el aria-selected se actualiza. Si el menú se cierra, el aria-expanded pasa a false. No hay que hacer nada manual.
Cuándo usarlo y cuándo no
Úsalo cuando:
Estás construyendo un design system propio
Necesitas componentes con markup y estilos completamente personalizados
Quieres cumplir WCAG sin depender de componentes de terceros
Tu app es utilizada por personas con lectores de pantalla o navegación por teclado
No lo necesitas cuando:
Usas Angular Material (ya tiene accesibilidad integrada)
Usas elementos HTML nativos para lo que son (<button>, <select>, <input>, <details>)
El componente no tiene interacción compleja
La regla de oro: si puedes usar HTML semántico nativo, úsalo. @angular/aria es para los casos donde no puedes.
Buenas prácticas
Empieza con el HTML semántico. Antes de agregar directivas de ARIA, pregúntate si hay un elemento HTML nativo que sirva. Un <button> ya es accesible. Un <a> ya es navegable. No necesitas directivas para eso.
No dupliques semántica. Si un elemento ya tiene role="button" de una directiva, no le agregues otro role manualmente. ARIA solo acepta un rol por elemento.
Siempre etiqueta los controles interactivos. Todo elemento interactivo necesita un nombre accesible: aria-label, aria-labelledby, o un <label> asociado. Las directivas de @angular/aria no inventan etiquetas; eso es responsabilidad del desarrollador.
<!-- Mal: la directiva aplica los roles, pero no hay nombre accesible -->
<input ngComboboxInput />
<!-- Bien: el nombre viene del label -->
<label id="country-label">País</label>
<input ngComboboxInput aria-labelledby="country-label" />
Testea con un lector de pantalla real. NVDA en Windows es gratuito. VoiceOver en Mac viene incluido. No basta con revisar visualmente que los atributos estén en el HTML. Hay que escuchar cómo se comporta el componente.
Agrega tests de accesibilidad automatizados. Librerías como axe-core o @angular-eslint pueden detectar errores comunes antes de que lleguen a producción.
// producto.component.spec.ts
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { axe, toHaveNoViolations } from 'jest-axe';
expect.extend(toHaveNoViolations);
it('no debe tener violaciones de accesibilidad', async () => {
const fixture = TestBed.createComponent(ProductoComponent);
fixture.detectChanges();
const results = await axe(fixture.nativeElement);
expect(results).toHaveNoViolations();
});
Lo que NO debes hacer
No uses ARIA para "decorar". ARIA modifica cómo los lectores de pantalla interpretan el contenido. Si aplicas role="button" a un <div> pero no implementas el comportamiento de teclado (Enter, Space), creaste algo peor que no tener nada.
No ocultes contenido relevante con aria-hidden="true" a lo loco. Si ocultas un elemento del árbol de accesibilidad, el lector de pantalla no puede llegar a él. Úsalo solo cuando el elemento es puramente decorativo.
<!-- Correcto: ícono decorativo que no aporta información -->
<svg aria-hidden="true" focusable="false">...</svg>
<!-- Incorrecto: ocultas contenido que el usuario necesita -->
<div aria-hidden="true">
<p>Este texto contiene información importante</p>
</div>
No confíes solo en el color para comunicar estado. Usar rojo para error y verde para éxito no basta para personas con daltonismo. Siempre acompaña el color con texto o iconos con significado.
No implementes patrones ARIA a medias. Un combobox sin gestión de foco es peor que un <input> simple. Si usas una directiva de @angular/aria, no la sobreescribas con atributos manuales que rompan el contrato.
Por qué esto importa más allá de la ley
En muchos países, la accesibilidad web es un requisito legal. La ADA en Estados Unidos, la EN 301 549 en Europa, y normas equivalentes en Latinoamérica implican que las aplicaciones públicas deben cumplir con WCAG 2.1 nivel AA como mínimo.
Pero más allá del cumplimiento legal, aproximadamente el 15% de la población mundial tiene alguna discapacidad. No son un nicho. Son usuarios reales que quieren usar tu producto.
Una app accesible también es una app más usable para todos. El contraste alto ayuda a quien lee en un día soleado. La navegación por teclado ayuda a usuarios de teclado mecánico o power users. Los textos alternativos en imágenes ayudan cuando la imagen no carga.
Casos de uso reales
Design system corporativo: Tu empresa tiene un kit de componentes propio. Con @angular/aria, cada componente del sistema (dropdowns, modales, tabs, menús) cumple WCAG sin tener que implementar los patrones a mano en cada equipo.
Portal de gobierno: Las apps del sector público en muchos países están obligadas a cumplir WCAG. @angular/aria reduce el esfuerzo de certificación significativamente.
Aplicación de recursos humanos: Los formularios complejos de selección, los selectores de fecha, las tablas de datos editables — todos se benefician de los patrones de Grid, Combobox y Listbox bien implementados.
App de e-commerce con catálogo filtrable: El filtro de categorías con checkboxes anidados, el selector de talla, el autocomplete de búsqueda — patrones que @angular/aria resuelve con consistencia.
Conclusión
La accesibilidad no es un feature opcional. Es parte de construir software que funcione para todas las personas, no solo para quienes tienen ratón, pantalla de alta resolución y visión perfecta.
@angular/aria no te hace accesible de forma automática. Te da las herramientas para serlo sin tener que convertirte en un experto de la especificación WAI-ARIA. La librería maneja los atributos, el teclado y el foco. Tú te encargas del HTML, el diseño y la lógica de negocio.
Es una división de responsabilidades que tiene sentido. Y en Angular 21, por fin está disponible de manera oficial.
Keywords
Angular ARIA
@angular/aria
Accesibilidad Angular
WAI-ARIA Angular
Angular componentes accesibles