---
name: accesibilidad-a11y
description: Checklist completo de accesibilidad WCAG 2.1 AA. Semántica HTML, ARIA, contraste de color, navegación por teclado, lectores de pantalla, formularios accesibles, tablas y multimedia. Con ejemplos MAL vs BIEN.
version: "1.0.0"
herramientasPermitidas: [Read, Grep]
exclusiones:
  - "No cargar para principios generales de UX (heurísticas de Nielsen, Gestalt, estados vacíos) — para UX cargar `ux-diseno`."
  - "No cargar para implementación de estilos CSS de foco o contraste — para CSS cargar `css-moderno` o `tailwind-experto`; este skill define los criterios, no la implementación."
  - "No cargar para auditorías de rendimiento web (Lighthouse performance, Core Web Vitals) — para rendimiento usar las herramientas de performance correspondientes."
  - "No cargar para accesibilidad de apps móviles nativas (iOS VoiceOver, Android TalkBack) — este skill cubre accesibilidad web; para iOS cargar `swift-experto`, para Android cargar `kotlin-experto`."
evolvable: true  # default para skill estandar
---
# Accesibilidad Web — WCAG 2.1 AA

## Cuándo NO cargar

- La tarea es sobre principios de UX: heurísticas de Nielsen, Gestalt, estados vacíos, feedback visual — cargar `ux-diseno`.
- El trabajo es implementar estilos CSS de foco o contraste de colores — para la implementación CSS cargar `css-moderno` o `tailwind-experto`; este skill define los criterios WCAG, no el CSS.
- El tema es rendimiento web: Lighthouse performance score, Core Web Vitals — usar herramientas de performance, no accesibilidad.
- El proyecto es una app móvil nativa: iOS con VoiceOver, Android con TalkBack — para iOS cargar `swift-experto`, para Android cargar `kotlin-experto`.

La accesibilidad no es opcional: en México, la NOM-019-SCFI-1998 y diversas leyes de inclusión
digital establecen obligaciones. WCAG 2.1 nivel AA es el estándar mínimo aceptable en producción.

---

## Los 4 Principios POUR

| Principio | Pregunta clave |
|-----------|---------------|
| **Perceptible** | ¿Pueden todos los usuarios percibir el contenido? |
| **Operable** | ¿Pueden todos los usuarios operar la interfaz? |
| **Comprensible** | ¿Es el contenido y la operación comprensibles? |
| **Robusto** | ¿Funciona con tecnologías de asistencia actuales y futuras? |

---

## 1. Semántica HTML — Reglas Obligatorias

- SIEMPRE usar elementos semánticos: `<nav>`, `<main>`, `<header>`, `<footer>`, `<article>`, `<aside>`.
- NUNCA usar `<div>` donde existe un elemento semántico equivalente.
- Un solo `<h1>` por página. Jerarquía `<h1>`-`<h6>` sin saltar niveles.
- Skip link OBLIGATORIO como primer elemento enfocable de la página.

```html
<!-- MAL: saltar niveles -->
<h1>Portal</h1>
<h3>Sección</h3>

<!-- BIEN: jerarquía continua -->
<h1>Portal</h1>
  <h2>Sección</h2>
    <h3>Subsección</h3>
```

---

## 2. Atributos ARIA — Regla de Oro

**HTML nativo primero**: NUNCA usar ARIA si el HTML semántico lo resuelve.

- `<button>` es mejor que `<div role="button">`.
- `aria-label` OBLIGATORIO en botones solo con ícono y en inputs sin label visible.
- `aria-expanded` en botones que abren/cierran contenido.
- `aria-live="polite"` para anuncios dinámicos.
- `aria-hidden="true"` en íconos decorativos.

```html
<!-- MAL: div clicable -->
<div onclick="enviar()" class="btn">Enviar</div>

<!-- BIEN: botón nativo -->
<button type="button" onclick="enviar()">Enviar</button>
```

---

## 3. Contraste de Color

| Tipo de texto | Ratio mínimo |
|---------------|-------------|
| Texto normal (<18pt o <14pt negrita) | 4.5:1 |
| Texto grande (>=18pt o >=14pt negrita) | 3:1 |
| Elementos UI y gráficos | 3:1 |

- NUNCA comunicar información solo mediante color (daltonismo). Acompañar con texto o ícono.
- Herramientas: WebAIM Contrast Checker, axe DevTools, Colour Contrast Analyser.

---

## 4. Navegación por Teclado

- NUNCA usar `tabindex` mayor que 0 — rompe el orden natural.
- OBLIGATORIO: `focus-visible` personalizado. NUNCA `outline: none` sin reemplazo.
- Los modales DEBEN atrapar el foco (focus trap) y regresar al trigger al cerrarse.
- `Escape` cierra modales y menús desplegables.

```css
/* BIEN: outline personalizado visible */
*:focus-visible {
  outline: 3px solid #005fcc;
  outline-offset: 2px;
}
```

---

## 5. Formularios Accesibles

- SIEMPRE asociar `<label>` a cada input con `for`/`id` o envolvente.
- NUNCA usar `placeholder` como único label.
- Campos requeridos: `aria-required="true"` + indicador visual.
- Errores: `aria-invalid="true"` + `aria-describedby` al mensaje + `role="alert"`.
- Grupos de radios/checkboxes en `<fieldset>` con `<legend>`.

```html
<label for="rfc">RFC</label>
<input type="text" id="rfc" name="rfc" aria-required="true" />
```

---

## 6. Imágenes y Multimedia

- Imágenes informativas: `alt` descriptivo del contenido, no del tipo.
- Imágenes decorativas: `alt=""` (vacío, no omitir el atributo).
- SVG: `role="img"` + `<title>` + `<desc>`.
- Videos: subtítulos en español obligatorios. Audiodescripción para contenido visual.
- NUNCA autoplay de audio sin interacción del usuario.

---

## 7. Lectores de Pantalla

- Clase `.sr-only` para texto visible solo a lectores de pantalla.
- Botones ambiguos ("X", "...") DEBEN tener `<span class="sr-only">` con contexto.
- Íconos decorativos: `aria-hidden="true"`.

---

## 8. Anti-patrones Críticos

| Anti-patrón | Corrección |
|-------------|------------|
| `outline: none` sin reemplazo | `focus-visible` personalizado |
| `<div onclick>` sin `role` | Usar `<button>` nativo |
| Placeholder como único label | `<label>` real asociado |
| Mensajes de error solo en color | Texto + ícono + color |
| `aria-hidden` en elemento enfocable | Quitar del flujo o remover hidden |

---

## 9. Auditoría automatizada vs manual — qué detecta cada una

Las herramientas automatizadas cubren solo el 30-40% de los criterios WCAG.
Saber qué detecta cada técnica evita falsa confianza y prioriza esfuerzo manual
donde realmente importa.

| Técnica | Qué detecta | Qué NO detecta |
|---------|-------------|----------------|
| **axe-core / pa11y** (lint estático) | Missing alt, missing labels, contraste insuficiente, roles ARIA inválidos, heading hierarchy rota | Flujo cognitivo, contexto de alt text, orden lógico de lectura, calidad de labels |
| **Lighthouse a11y audit** | Todo lo de axe + tabindex abuse, zooming disabled, link text genérico | Usabilidad real con lector de pantalla, focus trap correcto, timing de animaciones |
| **AX tree snapshot** (árbol de accesibilidad) | Estructura que el lector de pantalla percibe: roles, names, states, relaciones parent/child, focus order | Si el name es comprensible en contexto, si la experiencia tiene sentido narrativo |
| **Prueba manual con teclado** | Focus visible, tab order lógico, operabilidad sin mouse, escape cierra modales | Nada que requiera lector de pantalla (anuncios live, navegación por landmarks) |
| **Prueba con lector de pantalla** (NVDA/VoiceOver) | Todo lo anterior + experiencia real de navegación, anuncios aria-live, formularios completos | Nada — es la verificación más completa pero la más costosa |

**Gotcha**: Un score Lighthouse a11y de 100 NO significa que la interfaz es accesible.
Significa que pasó las verificaciones automatizables. Los problemas más graves
(flujo incomprensible, contexto perdido, interacciones rotas) solo se detectan
con lector de pantalla.

**Regla práctica**: axe/pa11y en CI (automático, cada commit) + prueba con teclado
por sprint + prueba con lector de pantalla por release.

---

## 10. Checklist Rápido

- [ ] Elementos interactivos accesibles por teclado
- [ ] Indicador de foco visible en todos los interactivos
- [ ] Contrastes WCAG 2.1 AA (4.5:1 normal, 3:1 grande)
- [ ] Todos los inputs con `<label>` o `aria-label`
- [ ] Modales con focus trap
- [ ] Imágenes con `alt` apropiado
- [ ] Sin `outline: none` sin reemplazo
- [ ] axe pasa sin errores A/AA
- [ ] Funcionalidad correcta a 200% de zoom

Para ejemplos completos de código, checklist detallado y patrones ARIA avanzados, ver [recursos/ejemplos-y-checklist-completo.md](recursos/ejemplos-y-checklist-completo.md).

## Gotchas / Errores comunes no obvios

**`aria-label` en un `<button>` sobreescribe el texto visible — el lector de pantalla anuncia el label, no el texto**: si un botón tiene `<button aria-label="Eliminar factura">X</button>`, el lector de pantalla anuncia "Eliminar factura" pero el usuario visual lee "X". Si el `aria-label` y el texto visible son inconsistentes, los usuarios que usan tanto visión como lector de pantalla (baja visión) se confunden. Causa: `aria-label` tiene prioridad sobre el contenido textual en el accessible name computation. Fix: para botones de ícono, usar `<button><span aria-hidden="true">✕</span><span class="sr-only">Eliminar factura</span></button>` en lugar de `aria-label`.

**Focus trap en un modal que usa `tabindex="0"` en el contenedor no funciona en todos los lectores de pantalla**: agregar `tabindex="0"` al `<div role="dialog">` y enfocar el contenedor no es suficiente — el usuario de NVDA puede escapar del modal con las teclas de navegación rápida (H, B, F para headings, botones, forms). Causa: las teclas de navegación rápida de lectores de pantalla saltan por encima del focus trap. Fix: implementar focus trap combinado: `inert` attribute en el contenido detrás del modal (estándar moderno) o la solución clásica con `aria-modal="true"` + listeners de `keydown` para Tab/Shift+Tab + `tabindex` negativo en elementos fuera del modal.

**`role="alert"` anuncia el contenido inmediatamente al montarse en el DOM, incluso si el mensaje no cambió**: si un componente de alerta usa `role="alert"` y el componente se remonta (por cambio de ruta o re-render), el lector de pantalla anuncia el contenido aunque sea el mismo mensaje que ya estaba. Causa: `role="alert"` es un live region atómico que anuncia cualquier cambio en su contenido, incluyendo la inserción inicial. Fix: usar `role="status"` para mensajes informativos no urgentes, o insertar el mensaje de forma diferida (con `setTimeout(fn, 100)`) para que el DOM esté estable antes de que el live region "escuche".

**Lighthouse a11y score de 100 no garantiza que los formularios complejos sean usables con lector de pantalla**: Lighthouse valida la presencia de labels y roles, pero no verifica si el contexto es comprensible — un input con `aria-label="Campo"` pasa Lighthouse pero es inútil para un usuario de NVDA que solo escucha "Campo, campo de edición". Causa: las herramientas automatizadas verifican estructura, no significado. Fix: probar el flujo completo del formulario con NVDA (Windows) o VoiceOver (macOS) navegando solo con teclado — los problemas reales de usabilidad solo se detectan así.

**Input con `<label>` wrapping (asociación implícita) falla tests E2E que buscan `id`/`aria-label`/`aria-labelledby`**: el patrón `<label>Email <input type="email" /></label>` es accesibilidad WCAG válida (asociación implícita), pero Playwright y Cypress suelen usar selectores como `page.getByLabel('Email')` que internamente buscan la propiedad `labels` del elemento o el `aria-labelledby`; en algunos navegadores/modos la asociación implícita no se refleja en `labels` cuando el input está dentro del label. Causa: el modelo DOM de labels implícitos depende de la implementación del navegador; los runners E2E no siempre resuelven la asociación igual que un screen reader. Fix: **agregar `id` explícito al input además del wrap**: `<label>Email <input id="email" type="email" /></label>`. La asociación implícita sigue siendo válida para el screen reader y el `id` permite al runner E2E encontrar el input con `page.locator('#email')` o `page.getByRole('textbox', {name: 'Email'})`. Esto no viola WCAG — agrega un mecanismo de selección redundante que no cambia el accessible name.
