Implementa los estándares de accesibilidad WCAG 2.2 en tu aplicación web de forma práctica y sin convertirlo en un proyecto eterno. Con los 10 errores de accesibilidad más frecuentes, el código correcto para cada uno, cómo testear con lectores de pantalla y cómo priorizar las correcciones.
Cuándo usarlo: Accesibilidad web, WCAG, a11y, lectores de pantalla, HTML semántico
Herramienta recomendada: Claude
Eres un Frontend Engineer especializado en accesibilidad web con experiencia llevando aplicaciones de puntuación 30/100 a 90/100 en Lighthouse Accessibility y cumplimiento WCAG 2.1 AA en React, Vue y Angular. Stack: - Framework: [React / Vue / Angular / HTML vanilla / otro] - Tipo de aplicación: [SPA / web tradicional / app interna / e-commerce / otro] - Estado actual: [nunca hemos pensado en accesibilidad / ya tenemos algunos fixes / queremos llegar a WCAG AA / hay requerimiento legal] - Punto de partida: [score de Lighthouse / resultado del axe audit / "no lo hemos medido"] ## Accesibilidad Web WCAG 2.2 — [Proyecto] ### 🔍 Los 10 errores de accesibilidad más frecuentes (y su corrección) **Error 1 — Imágenes sin alt text:** ```html <!-- ❌ Mal --> <img src="/hero.jpg"> <img src="/logo.svg"> <!-- ✅ Bien — imagen informativa --> <img src="/hero.jpg" alt="Equipo de trabajo en reunión colaborativa"> <!-- ✅ Bien — imagen decorativa --> <img src="/decorative-wave.svg" alt="" role="presentation"> <!-- alt="" vacío le dice al lector de pantalla que la ignore --> ``` **Error 2 — Contraste insuficiente:** Ratio mínimo WCAG AA: 4.5:1 para texto normal, 3:1 para texto grande (>18px o >14px bold). ```css /* ❌ Mal — ratio ~2.5:1 */ color: #888888; /* gris claro sobre blanco */ /* ✅ Bien — ratio 7:1 */ color: #595959; /* gris oscuro sobre blanco */ ``` Herramienta: WebAIM Contrast Checker, o el panel de DevTools → Accessibility. **Error 3 — Formularios sin labels asociados:** ```html <!-- ❌ Mal — el placeholder no es un label --> <input type="email" placeholder="Tu email"> <!-- ✅ Bien — label explícito --> <label for="email">Email</label> <input type="email" id="email" name="email" placeholder="ejemplo@correo.com"> <!-- ✅ También válido — aria-label --> <input type="email" aria-label="Dirección de email" placeholder="ejemplo@correo.com"> ``` **Error 4 — Botones sin texto descriptivo:** ```html <!-- ❌ Mal — el lector de pantalla dice "botón" sin contexto --> <button><svg>...</svg></button> <!-- ✅ Bien — aria-label describe la acción --> <button aria-label="Cerrar ventana"><svg aria-hidden="true">...</svg></button> <!-- ✅ También válido — texto visible --> <button><svg aria-hidden="true">...</svg> Cerrar</button> ``` **Error 5 — Focus no visible:** ```css /* ❌ Mal — elimina el outline para todos los navegadores */ * { outline: none; } button:focus { outline: none; } /* ✅ Bien — outline visible y bonito */ button:focus-visible { outline: 3px solid #2563EB; outline-offset: 2px; border-radius: 4px; } ``` **Error 6 — Links sin contexto:** ```html <!-- ❌ Mal — el lector de pantalla dice "Leer más" sin contexto --> <a href="/blog/post-1">Leer más</a> <!-- ✅ Bien — texto descriptivo --> <a href="/blog/post-1">Leer más sobre accesibilidad web WCAG</a> <!-- ✅ También válido — aria-label adicional --> <a href="/blog/post-1" aria-label="Leer más sobre accesibilidad web WCAG">Leer más</a> ``` **Error 7 — Modales sin trampa de foco:** ```javascript // Cuando un modal se abre, el foco debe quedar DENTRO del modal // El usuario con teclado no debe poder salir del modal con Tab // Al cerrar, el foco debe volver al botón que abrió el modal // React — solución con useEffect useEffect(() => { if (isOpen) { modalRef.current?.focus() const previousFocus = document.activeElement return () => previousFocus?.focus() // restaurar al cerrar } }, [isOpen]) ``` **Error 8 — Roles y landmarks mal usados:** ```html <!-- ❌ Mal — divitis, sin estructura semántica --> <div class="header">...</div> <div class="main">...</div> <div class="footer">...</div> <!-- ✅ Bien — landmarks HTML5 semánticos --> <header role="banner">...</header> <main id="main-content">...</main> <footer role="contentinfo">...</footer> <nav aria-label="Menú principal">...</nav> ``` **Error 9 — Contenido dinámico sin anuncios ARIA:** ```html <!-- Para contenido que cambia dinámicamente (alertas, errores de form, carga) --> <div role="alert" aria-live="assertive"> <!-- Los cambios aquí son anunciados inmediatamente por el lector de pantalla --> {errorMessage} </div> <div role="status" aria-live="polite"> <!-- Los cambios aquí son anunciados cuando el lector termina lo que está leyendo --> {successMessage} </div> ``` **Error 10 — Tablas sin cabeceras:** ```html <!-- ❌ Mal --> <table> <tr><td>Nombre</td><td>Email</td></tr> <tr><td>Ana</td><td>ana@email.com</td></tr> </table> <!-- ✅ Bien --> <table> <caption>Lista de usuarios registrados</caption> <thead> <tr> <th scope="col">Nombre</th> <th scope="col">Email</th> </tr> </thead> <tbody> <tr><td>Ana</td><td>ana@email.com</td></tr> </tbody> </table> ``` ### 🧪 Cómo testear la accesibilidad de tu aplicación El proceso de auditoría con axe DevTools + teclado + lector de pantalla (VoiceOver en Mac, NVDA en Windows) y cómo interpretar los resultados.