Por qué la accesibilidad web importa

La accesibilidad web (a11y) no es un extra ni un "nice to have". Es un requisito fundamental que beneficia a todos tus usuarios:

  • 1 de cada 5 personas tiene alguna forma de discapacidad
  • Usuarios con discapacidades temporales (brazo roto, infeccion ocular)
  • Usuarios en contextos situacionales (sol en la pantalla, manos ocupadas)
  • Usuarios mayores con vision o movilidad reducida
  • Mejora el SEO y la usabilidad general

Ademas, en muchos paises la accesibilidad web es un requisito legal. La European Accessibility Act entra en vigor en junio de 2025, y la ADA en Estados Unidos ya ha generado miles de demandas por sitios inaccesibles.

WCAG 2.2: los cuatro principios

Las Web Content Accessibility Guidelines se organizan en cuatro principios fundamentales, conocidos como POUR:

1. Perceptible

El contenido debe poder ser percibido por todos los sentidos disponibles.

2. Operable

La interfaz debe poder ser operada por cualquier usuario con cualquier dispositivo de entrada.

3. Comprensible

El contenido y la interfaz deben ser comprensibles.

4. Robusto

El contenido debe ser interpretable por tecnologias asistivas actuales y futuras.

Checklist completa por categoria

Estructura y semántica

  • Usar un único <main> por página
  • Encabezados en orden jerarquico sin saltos (h1 -> h2 -> h3)
  • Landmarks correctos: <header>, <nav>, <main>, <aside>, <footer>
  • Listas semanticas para grupos de items (<ul>, <ol>, <dl>)
  • Tablas con <caption>, <thead>, <th scope="col|row">
  • Idioma del documento declarado: <html lang="es">
  • Cambios de idioma marcados: <span lang="en">Responsive design</span>

Imagenes y multimedia

  • Todas las imagenes informativas tienen alt descriptivo
  • Imagenes decorativas usan alt="" o aria-hidden="true"
  • Graficos complejos tienen descripción larga con aria-describedby
  • Videos tienen subtitulos
  • Audio tiene transcripcion disponible
  • Las animaciones se pueden pausar o desactivar
  • Respetar prefers-reduced-motion:
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}
  • Todos los elementos interactivos son alcanzables con Tab
  • El orden de tabulacion es lógico y predecible
  • El foco es visible en todo momento (:focus-visible)
  • No hay trampas de teclado (el usuario puede salir con Tab/Escape)
  • Existe un "Skip to content" link
  • Los modales atrapan el foco correctamente (focus trapping)
  • Escape cierra modales y popups
  • Los shortcuts de teclado no interfieren con el navegador

Formularios

  • Cada input tiene un <label> asociado con for/id
  • Los grupos de inputs usan <fieldset> y <legend>
  • Los campos requeridos tienen aria-required="true" o required
  • Los errores se anuncian con aria-describedby y aria-invalid
  • El autocompletado usa autocomplete con valores correctos
  • Las instrucciones de formato estan visibles, no solo como placeholder
<div class="form-group">
  <label for="email">Correo electronico</label>
  <input
    id="email"
    type="email"
    required
    autocomplete="email"
    aria-describedby="email-help email-error"
  />
  <p id="email-help" class="hint">Ejemplo: [email protected]</p>
  <p id="email-error" class="error" role="alert" aria-live="assertive">
    <!-- Mensaje de error dinámico -->
  </p>
</div>

Color y contraste

  • Ratio de contraste mínimo 4.5:1 para texto normal (AA)
  • Ratio de contraste mínimo 3:1 para texto grande (24px+ o 19px+ bold)
  • Ratio de contraste mínimo 3:1 para componentes de UI y graficos
  • La información no se transmite solo por color
  • Funciona correctamente en modo de alto contraste

Verificar contraste programaticamente

// Calcular ratio de contraste entre dos colores
function contrastRatio(luminance1, luminance2) {
  const lighter = Math.max(luminance1, luminance2);
  const darker = Math.min(luminance1, luminance2);
  return (lighter + 0.05) / (darker + 0.05);
}

function relativeLuminance(r, g, b) {
  const [rs, gs, bs] = [r, g, b].map(c => {
    c = c / 255;
    return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
  });
  return 0.2126 * rs + 0.7152 * gs + 0.0722 * bs;
}

Contenido dinámico y ARIA

  • Los cambios dinámicos se anuncian con aria-live
  • Los estados de carga usan aria-busy="true"
  • Los tabs usan roles tablist, tab, tabpanel
  • Los accordions usan aria-expanded
  • Los menus usan roles menu, menuitem
  • Los tooltips se vinculan con aria-describedby
  • Los botones de toggle usan aria-pressed
<!-- Tabs accesibles -->
<div role="tablist" aria-label="Secciones del curso">
  <button role="tab"
          id="tab-1"
          aria-selected="true"
          aria-controls="panel-1">
    Leccion 1
  </button>
  <button role="tab"
          id="tab-2"
          aria-selected="false"
          aria-controls="panel-2"
          tabindex="-1">
    Leccion 2
  </button>
</div>

<div role="tabpanel"
     id="panel-1"
     aria-labelledby="tab-1">
  Contenido de la leccion 1...
</div>

<div role="tabpanel"
     id="panel-2"
     aria-labelledby="tab-2"
     hidden>
  Contenido de la leccion 2...
</div>

Manejo de foco en SPAs

En aplicaciones de una sola página (SPA), el manejo del foco es crítico:

// Angular: mover el foco al contenido principal despues de navegar
import { Router, NavigationEnd } from '@angular/router';
import { inject } from '@angular/core';

export class AppComponent {
  private router = inject(Router);

  constructor() {
    this.router.events.subscribe(event => {
      if (event instanceof NavigationEnd) {
        const main = document.querySelector('main');
        if (main) {
          main.setAttribute('tabindex', '-1');
          main.focus();
        }
      }
    });
  }
}

Herramientas de testing

Automatizadas

Herramienta Tipo Cobertura
axe DevTools Extension de navegador ~57% de issues WCAG
Lighthouse Chrome DevTools Auditoria general
pa11y CLI / CI Automatizacion
eslint-plugin-jsx-a11y Linter Prevencion en código
jest-axe Testing unitario Tests automatizados

Testing manual imprescindible

Las herramientas automatizadas solo detectan el 30-50% de los problemas de accesibilidad. El testing manual es obligatorio:

  1. Navegar solo con teclado: Tab, Shift+Tab, Enter, Espacio, Escape, flechas
  2. Usar un lector de pantalla: NVDA (Windows), VoiceOver (Mac/iOS), TalkBack (Android)
  3. Verificar con zoom al 200%: todo debe ser funcional y legible
  4. Probar sin colores: usar extensión de simulacion de daltonismo
  5. Verificar en modo de alto contraste: Windows High Contrast Mode

Los 10 errores más comunes

  1. Imagenes sin atributo alt
  2. Formularios sin labels asociados
  3. Contraste insuficiente
  4. No hay indicador de foco visible
  5. Links genericos como "click aquí" o "leer más" sin contexto
  6. Contenido solo accesible con mouse (hover)
  7. Modales sin focus trapping
  8. No hay skip link
  9. Videos sin subtitulos
  10. Uso incorrecto de ARIA (peor que no usar ARIA)

Conclusion

La accesibilidad web no es una fase del desarrollo, es una filosofia de diseño. Cada decision, desde la estructura HTML hasta los colores y las interacciones, impacta en la experiencia de millones de personas. Esta checklist es un punto de partida solido, pero la verdadera accesibilidad viene de empatizar con tus usuarios y probar continuamente con personas reales y tecnologias asistivas.