Accesibilidad Web en la Práctica: Roles ARIA y Guía de Cumplimiento WCAG 2.2

前端工程

Los Cuatro Principios de WCAG 2.2

WCAG 2.2 organiza los requisitos de cumplimiento en torno a los principios POUR:

Principio Significado Requisito Central Ejemplo
Perceptible La información es presentable Contenido perceptible por todos los usuarios Alt de imagen, subtítulos, contraste
Operable La interfaz es operable Utilizable mediante múltiples métodos de entrada Navegación por teclado, tiempo suficiente, sin parpadeo
Comprensible El contenido es comprensible El contenido y la operación son claros Navegación consistente, sugerencias de error, ayuda de entrada
Robusto El contenido es compatible Analizable por tecnologías de asistencia HTML válido, semántica ARIA

Sistema de Roles ARIA

Roles de Estructura del Documento

<header role="banner">
  <nav role="navigation" aria-label="Main navigation">
    <ul role="menubar">
      <li role="menuitem"><a href="/home">Home</a></li>
      <li role="menuitem" aria-haspopup="true">
        Products
        <ul role="menu">
          <li role="menuitem"><a href="/saas">SaaS</a></li>
          <li role="menuitem"><a href="/api">API</a></li>
        </ul>
      </li>
    </ul>
  </nav>
</header>
<main role="main">
  <article role="article">
    <h2>Article Title</h2>
    <p>Content body</p>
  </article>
</main>
<footer role="contentinfo">Copyright info</footer>

Roles de Widget

Rol Propósito Propiedades Requeridas Interacción Clave
button Disparador clickeable Ninguna Enter/Espacio activa
checkbox Marcado/desmarcado aria-checked Espacio alterna
radio Opción única aria-checked Flechas cambian
slider Valor de rango aria-valuenow, aria-valuemin, aria-valuemax Flechas ajustan
tablist / tab Paneles de pestañas aria-selected Flechas cambian
dialog Modal/no modal aria-modal Esc cierra
combobox Entrada seleccionable aria-expanded Flecha seleccionar/filtro de escritura
tree / treeitem Estructura de árbol aria-expanded Flecha expandir/colapsar

Propiedades y Estados ARIA

Referencia Rápida de Propiedades Core

<!-- Labels and descriptions -->
<button aria-label="Close dialog">✕</button>
<input aria-labelledby="name-label" aria-describedby="name-hint">
<span id="name-label">Username</span>
<span id="name-hint">6-20 characters</span>

<!-- States -->
<div aria-expanded="true" aria-selected="false" aria-checked="mixed">
<div aria-hidden="true" aria-disabled="true" aria-busy="true">

<!-- Relationships -->
<div aria-owns="child-id" aria-controls="panel-id" aria-flowto="next-section">

<!-- Live regions -->
<div role="alert" aria-live="assertive">Error message</div>
<div aria-live="polite" aria-atomic="true" aria-relevant="additions text">Status update</div>

Primera Regla: No Abuse de ARIA

Si un elemento HTML nativo ya proporciona la semántica y la interacción necesarias, no lo reimplemente con ARIA.

<!-- ❌ Anti-pattern -->
<div role="button" tabindex="0" onclick="submit()">Submit</div>

<!-- ✅ Correct -->
<button type="submit">Submit</button>

Práctica: Modal Accesible

<div class="modal-overlay" id="modalOverlay" hidden>
  <div
    role="dialog"
    aria-modal="true"
    aria-labelledby="modalTitle"
    aria-describedby="modalDesc"
    class="modal"
  >
    <h2 id="modalTitle">Confirm Deletion</h2>
    <p id="modalDesc">This action cannot be undone. Are you sure you want to delete this record?</p>
    <div class="modal-actions">
      <button class="btn-cancel">Cancel</button>
      <button class="btn-confirm">Confirm Delete</button>
    </div>
  </div>
</div>
class AccessibleModal {
  constructor(dialogEl) {
    this.dialog = dialogEl;
    this.previousFocus = null;
  }

  open() {
    this.previousFocus = document.activeElement;
    this.dialog.hidden = false;
    const firstFocusable = this.dialog.querySelector(
      'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
    );
    firstFocusable?.focus();
    document.addEventListener('keydown', this.handleKeydown);
  }

  close() {
    this.dialog.hidden = true;
    this.previousFocus?.focus();
    document.removeEventListener('keydown', this.handleKeydown);
  }

  handleKeydown = (e) => {
    if (e.key === 'Escape') this.close();
    if (e.key === 'Tab') this.trapFocus(e);
  };

  trapFocus(e) {
    const focusable = [...this.dialog.querySelectorAll(
      'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
    )];
    const first = focusable[0];
    const last = focusable[focusable.length - 1];
    if (e.shiftKey && document.activeElement === first) {
      e.preventDefault();
      last.focus();
    } else if (!e.shiftKey && document.activeElement === last) {
      e.preventDefault();
      first.focus();
    }
  }
}

Práctica: Pestañas Accesibles

<div class="tabs">
  <div role="tablist" aria-label="Account settings">
    <button
      role="tab"
      id="tab-profile"
      aria-selected="true"
      aria-controls="panel-profile"
      tabindex="0"
    >Profile</button>
    <button
      role="tab"
      id="tab-security"
      aria-selected="false"
      aria-controls="panel-security"
      tabindex="-1"
    >Security</button>
    <button
      role="tab"
      id="tab-notify"
      aria-selected="false"
      aria-controls="panel-notify"
      tabindex="-1"
    >Notifications</button>
  </div>

  <div role="tabpanel" id="panel-profile" aria-labelledby="tab-profile">
    <p>Profile settings content</p>
  </div>
  <div role="tabpanel" id="panel-security" aria-labelledby="tab-security" hidden>
    <p>Security settings content</p>
  </div>
  <div role="tabpanel" id="panel-notify" aria-labelledby="tab-notify" hidden>
    <p>Notification preferences content</p>
  </div>
</div>
document.querySelector('[role="tablist"]').addEventListener('keydown', (e) => {
  const tabs = [...e.currentTarget.querySelectorAll('[role="tab"]')];
  const idx = tabs.indexOf(document.activeElement);
  let nextIdx;

  if (e.key === 'ArrowRight') nextIdx = (idx + 1) % tabs.length;
  else if (e.key === 'ArrowLeft') nextIdx = (idx - 1 + tabs.length) % tabs.length;
  else if (e.key === 'Home') nextIdx = 0;
  else if (e.key === 'End') nextIdx = tabs.length - 1;
  else return;

  e.preventDefault();
  tabs.forEach((t) => { t.setAttribute('aria-selected', 'false'); t.tabIndex = -1; });
  tabs[nextIdx].setAttribute('aria-selected', 'true');
  tabs[nextIdx].tabIndex = 0;
  tabs[nextIdx].focus();
});

Práctica: Formulario Accesible

<form aria-label="User registration">
  <div class="field">
    <label for="email">Email address <span aria-hidden="true">*</span></label>
    <input
      id="email"
      type="email"
      required
      aria-required="true"
      autocomplete="email"
      aria-invalid="false"
      aria-describedby="email-error"
    >
    <span id="email-error" role="alert" aria-live="assertive"></span>
  </div>

  <div class="field">
    <label for="password">Password</label>
    <input
      id="password"
      type="password"
      minlength="8"
      aria-describedby="pwd-rules pwd-strength"
    >
    <span id="pwd-rules">At least 8 characters, including uppercase, lowercase, and digits</span>
    <span id="pwd-strength" aria-live="polite"></span>
  </div>

  <fieldset>
    <legend>Notification method</legend>
    <label><input type="checkbox" name="notify" value="email"> Email</label>
    <label><input type="checkbox" name="notify" value="sms"> SMS</label>
  </fieldset>

  <button type="submit">Register</button>
</form>

Comparación de Herramientas de Prueba Automatizadas

Herramienta Tipo Cobertura WCAG Integración Característica
axe-core Biblioteca en tiempo de ejecución A/AA CI + Navegador La más completa, cero falsos positivos
Lighthouse Herramienta de auditoría A CLI / DevTools Rendimiento combinado + a11y
WAVE Extensión de navegador A/AA Manual Anotaciones visuales
NVDA Lector de pantalla Manual Experiencia real del usuario
Playwright a11y Prueba E2E A CI Integrada con el flujo de pruebas
import { test } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';

test('accessibility check', async ({ page }) => {
  await page.goto('/register');
  const results = await new AxeBuilder({ page })
    .withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'])
    .analyze();
  expect(results.violations).toEqual([]);
});

Nuevos Criterios de WCAG 2.2

Nuevo Criterio Nivel Descripción
2.4.11 Apariencia del Foco AAA Área del indicador de foco ≥ 2px, contraste ≥ 3:1
2.4.12 Foco No Oscurecido AAA Elemento enfocado no oscurecido por otro contenido
3.2.6 Ayuda Consistente A Ubicación del mecanismo de ayuda consistente entre páginas
3.3.7 Entrada Redundante A No requerir reingresar información proporcionada previamente
3.3.8 Autenticación Accesible AA La autenticación no depende de pruebas de función cognitiva

Resumen de Mejores Prácticas

  1. Elementos nativos primero: Use <button>, <input> etc. en lugar de reimplementación con ARIA
  2. Gestión del foco: Enfoque proactivamente a la posición lógica después de cambios de modal/ruta
  3. Regiones en vivo con cuidado: Use aria-live="assertive" solo para notificaciones críticas
  4. Automatizado + manual: axe-core cubre ~30-40% de los problemas; las pruebas con lector de pantalla son esenciales
  5. Integración continua: Incluya verificaciones de accesibilidad en el pipeline de CI para prevenir regresiones

Prueba estas herramientas que se ejecutan en tu navegador — no requieren registro →

#无障碍#ARIA#WCAG#屏幕阅读器#可访问性