Arquitectura de los Screen Readers y el Árbol de Accesibilidad

Comprender la arquitectura interna de los screen readers permite tomar mejores decisiones de marcado.

Cuando una página carga, el screen reader construye un árbol de accesibilidad, una representación paralela del DOM que elimina la información puramente visual y expone la estructura semántica:

Elemento DOM → Rol de Accesibilidad
<button> → button
<a href="..."> → link
<h1> → heading (nivel 1)
<input type="text"> → textbox
<div> → generic (ignorado por la mayoría de SR)
<nav> → navigation
<main> → main

El screen reader navega este árbol, no el DOM. Por eso el HTML semántico importa tanto. Un <div onclick="..."> no tiene rol de accesibilidad. Es invisible en el árbol.

<!-- MAL: invisible para screen readers sin ARIA extensivo -->
<div class="btn" onclick="handleClick()">Enviar</div>

<!-- BIEN: utilizar directamente un button -->
<button class="btn" onclick="handleClick()">Enviar</button>

ARIA Live Regions: Haciendo Accesible el Contenido Dinámico

Las ARIA live regions le dicen a los screen readers que monitoreen una sección del DOM por cambios y los anuncien:

// live_regions.js - Implementación correcta de live regions
class LiveAnnouncer {
 constructor() {
 this.politeRegion = this.createRegion('polite');
 this.assertiveRegion = this.createRegion('assertive');
 document.body.appendChild(this.politeRegion);
 document.body.appendChild(this.assertiveRegion);
 }

 createRegion(politeness) {
 const region = document.createElement('div');
 region.setAttribute('aria-live', politeness);
 region.setAttribute('aria-atomic', 'true');
 // Visualmente oculto pero accesible para screen readers
 Object.assign(region.style, {
 position: 'absolute',
 width: '1px', height: '1px',
 padding: '0', margin: '-1px',
 overflow: 'hidden',
 clip: 'rect(0, 0, 0, 0)',
 whiteSpace: 'nowrap', border: '0',
 });
 return region;
 }

 announce(message, priority = 'polite') {
 const region = priority === 'assertive'
 ? this.assertiveRegion : this.politeRegion;
 // Limpiar y re-setear para disparar el anuncio
 region.textContent = '';
 requestAnimationFrame(() => {
 region.textContent = message;
 });
 }
}

const announcer = new LiveAnnouncer();
announcer.announce('23 resultados encontrados para "accesibilidad"');
announcer.announce('El email es inválido. Verificar el formato.', 'assertive');

La técnica del requestAnimationFrame es importante. Algunos screen readers (especialmente VoiceOver) no anuncian cambios si el contenido se establece en el mismo frame que se limpió.

Manejo de Foco: La UX Invisible

El manejo de foco es el aspecto más impactante y más descuidado de la accesibilidad web.

// focus_management.js
class FocusManager {
 handleRouteChange(pageTitle) {
 document.title = pageTitle;
 const mainHeading = document.querySelector('main h1');
 if (mainHeading) {
 if (!mainHeading.hasAttribute('tabindex')) {
 mainHeading.setAttribute('tabindex', '-1');
 }
 mainHeading.focus();
 }
 }

 trapFocus(modalElement) {
 const previousFocus = document.activeElement;
 const focusableElements = modalElement.querySelectorAll(
 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
 );
 const firstFocusable = focusableElements[0];
 const lastFocusable = focusableElements[focusableElements.length - 1];

 firstFocusable?.focus();

 const trapHandler = (e) => {
 if (e.key !== 'Tab') return;
 if (e.shiftKey) {
 if (document.activeElement === firstFocusable) {
 e.preventDefault();
 lastFocusable?.focus();
 }
 } else {
 if (document.activeElement === lastFocusable) {
 e.preventDefault();
 firstFocusable?.focus();
 }
 }
 };
 modalElement.addEventListener('keydown', trapHandler);
 return () => {
 modalElement.removeEventListener('keydown', trapHandler);
 previousFocus?.focus();
 };
 }
}

Testing Automatizado con axe-core

// axe-ci.js - Integración de axe-core para CI/CD
import { AxeBuilder } from '@axe-core/playwright';
import { test, expect } from '@playwright/test';

const pages = [
 { url: '/', name: 'Home' },
 { url: '/products', name: 'Productos' },
 { url: '/cart', name: 'Carrito' },
 { url: '/checkout', name: 'Checkout' },
];

for (const page of pages) {
 test(`accesibilidad: ${page.name}`, async ({ page: pwPage }) => {
 await pwPage.goto(page.url);
 await pwPage.waitForLoadState('networkidle');

 const results = await new AxeBuilder({ page: pwPage })
 .withTags(['wcag2a', 'wcag2aa', 'wcag22aa'])
 .exclude('.third-party-widget')
 .analyze();

 const violations = results.violations.map(v => ({
 rule: v.id,
 impact: v.impact,
 description: v.description,
 nodes: v.nodes.map(n => ({
 html: n.html.substring(0, 200),
 target: n.target,
 failureSummary: n.failureSummary,
 }))
 }));

 expect(violations).toEqual([]);
 });
}

Metodología de Auditoría de Accesibilidad en Cuatro Fases

Un enfoque probado de cuatro fases cubre el espectro completo de problemas de accesibilidad:

Fase 1: Scan Automatizado (atrapa ~30% de issues)

  • Ejecutar axe-core en cada página y estado interactivo
  • Verificar contraste de color con herramientas automatizadas
  • Validar HTML (HTML inválido rompe screen readers)

Fase 2: Navegación por Teclado (atrapa ~25% de issues)

  • Tab por cada página, verificar indicador de foco visible
  • Testear todos los elementos interactivos con Enter, Space, Escape, Arrow keys
  • Verificar que el orden de foco coincida con el orden visual
  • Verificar el confinamiento de foco en modales

Fase 3: Testing con Screen Reader (atrapa ~35% de issues)

  • Navegar con NVDA/VoiceOver en modo forms y modo browse
  • Verificar que todas las imágenes tengan alt text significativo
  • Verificar que el contenido dinámico se anuncie
  • Verificar widgets personalizados (selectores de fecha, acordeones, pestañas)

Fase 4: Cognitivo y Motor (atrapa ~10% de issues)

  • Testear con zoom 200%
  • Testear con movimiento reducido (prefers-reduced-motion)
  • Verificar que los targets de toque sean al menos 44x44px

Lecciones de Fallos de Accesibilidad en Notificaciones

Cuando un usuario de screen reader reporta notificaciones invisibles, la causa raíz es casi siempre inyección ad-hoc de DOM sin consideración de accesibilidad. Tres cambios abordan este patrón de manera efectiva:

  1. Toda actualización de contenido dinámico debe pasar por un servicio centralizado de LiveAnnouncer. No más inyección ad-hoc de DOM sin consideración de accesibilidad.

  2. El testing con screen reader pertenece al checklist de QA. No simplemente "verificar el scan de axe", se requiere testing real con NVDA de cada funcionalidad nueva.

  3. Contratar un consultor que sea usuario diario de screen reader para revisiones trimestrales provee una perspectiva irremplazable. Alguien que realmente depende de la tecnología de accesibilidad detecta problemas que los testers videntes consistentemente pasan por alto.

Los tests automatizados son necesarios pero insuficientes. El testing manual consume tiempo pero es esencial. El feedback de los usuarios es lo que marca la diferencia entre compliance y accesibilidad real. Una feature que falle un check automatizado pero funcione perfecto con un screen reader es más valiosa que una que pase todos los scans automatizados pero deje varados a los usuarios reales.