Communitygithub.com

Patrones de Accesibilidad en el Frontend

Patrones prácticos de accesibilidad para React y Next.js. Cubre los problemas más comunes detectados en revisiones de código: etiquetas de formulario faltantes, uso incorrecto de ARIA, elementos interactivos no semánticos y navegación por teclado rota.

¿Qué es Patrones de Accesibilidad en el Frontend?

Este skill proporciona una guía completa de patrones de accesibilidad para el frontend, con ejemplos específicos para React y Next.js. Está diseñado para ayudar a desarrolladores a identificar y corregir los problemas de accesibilidad más frecuentes durante las revisiones de código, como la falta de conexión entre etiquetas y campos de formulario, el uso incorrecto de atributos ARIA, el uso de elementos no semánticos para interacciones y la navegación por teclado defectuosa. El skill incluye ejemplos detallados de código correcto e incorrecto para formularios accesibles, HTML semántico, atributos ARIA, navegación por teclado, gestión del foco en modales, imágenes e íconos, y respeto por la preferencia de movimiento reducido del usuario. Además, ofrece una lista de verificación útil para aplicar antes de enviar cualquier componente interactivo a revisión. Es compatible con plataformas que utilizan React y Next.js, y se activa al construir o revisar componentes de formulario, elementos interactivos, o al recibir feedback de herramientas de revisión de código como CodeRabbit o ESLint a11y.

Compatible con~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/affaan-m/ECC/tree/main/skills/frontend-a11y

Preguntar en tu IA favorita

Abre un nuevo chat con esta habilidad de agente ya precargada.

Vista previa

Del README del skill

Patrones de Accesibilidad en el Frontend preview
Patrones de Accesibilidad en el Frontend preview
Patrones de Accesibilidad en el Frontend preview

Documentación

¿Qué hace Patrones de Accesibilidad en el Frontend?

Patrones prácticos de accesibilidad para React y Next.js. Cubre los problemas más comunes detectados en revisiones de código: etiquetas de formulario faltantes, uso incorrecto de ARIA, elementos interactivos no semánticos y navegación por teclado rota.

Cuándo Activar

  • Al construir o revisar componentes de formulario (<input>, <select>, <textarea>)
  • Al crear elementos interactivos (modales, menús desplegables, tooltips, pestañas)
  • Al usar <div> o <span> con onClick
  • Al agregar atributos aria-* a cualquier elemento
  • Al implementar navegación por teclado o gestión del foco
  • Al recibir comentarios de accesibilidad de herramientas de revisión de código (CodeRabbit, ESLint a11y)
  • Al construir componentes que deben ser compatibles con lectores de pantalla

Accesibilidad en Formularios

La falta de emparejamiento htmlFor / id y los mensajes de error desconectados son los problemas más comunes detectados en las revisiones de código.

Conexión de Etiqueta

// MAL: la etiqueta no tiene conexión con el input — los lectores de pantalla no pueden asociarlos
<label>Email</label>
<input type="email" />

// BIEN: htmlFor coincide con el id del input
<label htmlFor="email">Email</label>
<input id="email" type="email" />

Campos Requeridos

// MAL: el asterisco solo visual no transmite nada a los lectores de pantalla
<label htmlFor="email">Email *</label>
<input id="email" type="email" />

// BIEN: required habilita la validación nativa del navegador; aria-required lo señala a los lectores de pantalla
<label htmlFor="email">
  Email <span aria-hidden="true">*</span>
</label>
<input id="email" type="email" required aria-required="true" />

Mensajes de Error

// MAL: el texto de error existe visualmente pero no está vinculado al input
<input id="email" type="email" />
<span className="error">Dirección de correo inválida</span>

// BIEN: aria-describedby conecta el input con su mensaje de error
// aria-invalid señala el estado inválido a los lectores de pantalla
<input
  id="email"
  type="email"
  aria-describedby="email-error"
  aria-invalid={!!error}
/>
{error && (
  <span id="email-error" role="alert">
    {error}
  </span>
)}

Formulario Accesible Completo

interface LoginFormProps {
  onSubmit: (email: string, password: string) => void;
}

export function LoginForm({ onSubmit }: LoginFormProps) {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [errors, setErrors] = useState<{ email?: string; password?: string }>({});

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    const newErrors: typeof errors = {};
    if (!email) newErrors.email = 'El email es obligatorio';
    if (!password) newErrors.password = 'La contraseña es obligatoria';
    if (Object.keys(newErrors).length) {
      setErrors(newErrors);
      return;
    }
    onSubmit(email, password);
  };

  return (
    <form onSubmit={handleSubmit} noValidate>
      <div>
        <label htmlFor="email">
          Email <span aria-hidden="true">*</span>
        </label>
        <input
          id="email"
          type="email"
          value={email}
          onChange={e => setEmail(e.target.value)}
          aria-required="true"
          aria-describedby={errors.email ? 'email-error' : undefined}
          aria-invalid={!!errors.email}
          autoComplete="email"
        />
        {errors.email && (
          <span id="email-error" role="alert">
            {errors.email}
          </span>
        )}
      </div>

      <div>
        <label htmlFor="password">
          Contraseña <span aria-hidden="true">*</span>
        </label>
        <input
          id="password"
          type="password"
          value={password}
          onChange={e => setPassword(e.target.value)}
          aria-required="true"
          aria-describedby={errors.password ? 'password-error' : undefined}
          aria-invalid={!!errors.password}
          autoComplete="current-password"
        />
        {errors.password && (
          <span id="password-error" role="alert">
            {errors.password}
          </span>
        )}
      </div>

      <button type="submit">Iniciar sesión</button>
    </form>
  );
}

HTML Semántico

Usa el elemento que coincida con la intención. Los lectores de pantalla y los usuarios de teclado dependen de la semántica nativa.

// MAL: div no tiene rol, ni soporte de teclado, ni nombre accesible
<div onClick={handleClick}>Enviar</div>

// BIEN: button es enfocable, se activa con Enter/Espacio, se anuncia como "botón"
<button type="button" onClick={handleClick}>Enviar</button>
// MAL: navegación no semántica
<div onClick={() => navigate('/home')}>Inicio</div>

// BIEN: anchor soporta clic derecho, clic central y navegación por teclado
<a href="/home">Inicio</a>
// MAL: jerarquía de encabezados saltada (h1 a h4)
<h1>Tablero</h1>
<h4>Actividad Reciente</h4>

// BIEN: niveles de encabezado secuenciales
<h1>Tablero</h1>
<h2>Actividad Reciente</h2>

Atributos ARIA

Usa ARIA solo cuando la semántica HTML nativa sea insuficiente. Un ARIA incorrecto es peor que no usar ARIA.

aria-label vs aria-labelledby

// aria-label: etiqueta de cadena en línea — úsalo cuando no exista texto de etiqueta visible
<button aria-label="Cerrar modal">
  <XIcon />
</button>

// aria-labelledby: referencia el texto de otro elemento — úsalo cuando exista una etiqueta visible
<section aria-labelledby="section-title">
  <h2 id="section-title">Pedidos Recientes</h2>
  {/* contenido */}
</section>

aria-describedby

// Proporciona una descripción complementaria más allá de la etiqueta
<button
  aria-describedby="delete-warning"
  onClick={handleDelete}
> Eliminar cuenta
</button>
<p id="delete-warning">Esta acción no se puede deshacer.</p>

aria-live para Contenido Dinámico

// Usa aria-live para anunciar contenido que se actualiza sin recargar la página
// polite: espera a que el usuario termine la acción actual antes de anunciar
// assertive: interrumpe inmediatamente — úsalo solo para errores urgentes

export function StatusMessage({ message, isError }: { message: string; isError?: boolean }) {
  return (
    <div role="status" aria-live={isError ? 'assertive' : 'polite'} aria-atomic="true">
      {message}
    </div>
  );
}

aria-expanded y aria-controls

export function Accordion({ title, children }: { title: string; children: React.ReactNode }) {
  const [isOpen, setIsOpen] = useState(false);
  const contentId = useId();

  return (
    <div>
      <button aria-expanded={isOpen} aria-controls={contentId} onClick={() => setIsOpen(prev => !prev)}>
        {title}
      </button>
      <div id={contentId} hidden={!isOpen}>
        {children}
      </div>
    </div>
  );
}

Navegación por Teclado

Cada elemento interactivo debe ser alcanzable y operable solo con el teclado.

Menú Desplegable Personalizado

export function Dropdown({ options, onSelect }: { options: string[]; onSelect: (value: string) => void }) {
  const [isOpen, setIsOpen] = useState(false);
  const [activeIndex, setActiveIndex] = useState(0);
  const listId = useId();

  if (!options.length) return null;

  const handleKeyDown = (e: React.KeyboardEvent) => {
    switch (e.key) {
      case 'ArrowDown':
        e.preventDefault();
        setActiveIndex(i => Math.min(i + 1, options.length - 1));
        break;
      case 'ArrowUp':
        e.preventDefault();
        setActiveIndex(i => Math.max(i - 1, 0));
        break;
      case 'Enter':
      case ' ':
        e.preventDefault();
        if (isOpen) onSelect(options[activeIndex]);
        setIsOpen(prev => !prev);
        break;
      case 'Escape':
        setIsOpen(false);
        break;
    }
  };

  return (
    <div
      role="combobox"
      aria-expanded={isOpen}
      aria-haspopup="listbox"
      aria-controls={listId}
      tabIndex={0}
      onKeyDown={handleKeyDown}
      onClick={() => setIsOpen(prev => !prev)}
    >
      <span>{options[activeIndex]}</span>
      {isOpen && (
        <ul id={listId} role="listbox">
          {options.map((option, index) => (
            <li
              key={option}
              role="option"
              aria-selected={index === activeIndex}
              onClick={() => {
                onSelect(option);
                setIsOpen(false);
              }}
            >
              {option}
            </li>
          ))}
        </ul>
      )}
    </div>
  );
}

Gestión del Foco

El foco debe moverse de manera lógica cuando cambia el estado de la interfaz de usuario, especialmente para modales y transiciones de ruta.

Restauración del Foco en un Modal

Este ejemplo cubre el foco inicial y la restauración. Para un bloqueo de foco completo (desplazamiento cíclico con Tab/Shift+Tab dentro del modal), usa una librería como focus-trap-react que maneja casos extremos como contenido dinámico y portales anidados.

export function Modal({ isOpen, onClose, title, children }: { isOpen: boolean; onClose: () => void; title: string; children: React.ReactNode }) {
  const modalRef = useRef<HTMLDivElement>(null);
  const previousFocusRef = useRef<HTMLElement | null>(null);

  useEffect(() => {
    if (isOpen) {
      // Guarda el elemento actualmente enfocado y mueve el foco al modal
      previousFocusRef.current = document.activeElement as HTMLElement;
      modalRef.current?.focus();
    } else {
      // Restaura el foco al elemento que abrió el modal
      previousFocusRef.current?.focus();
    }
  }, [isOpen]);

  if (!isOpen) return null;

  return (
    <div ref={modalRef} role="dialog" aria-modal="true" aria-labelledby="modal-title" tabIndex={-1} onKeyDown={e => e.key === 'Escape' && onClose()}>
      <h2 id="modal-title">{title}</h2>
      {children}
      <button onClick={onClose}>Cerrar</button>
    </div>
  );
}

Imágenes e Íconos

// MAL: ícono decorativo anunciado como imagen sin etiqueta
<img src="/icon.svg" />

// BIEN: imagen decorativa oculta para los lectores de pantalla
<img src="/decoration.png" alt="" aria-hidden="true" />

// BIEN: imagen significativa con texto alternativo descriptivo
<img src="/chart.png" alt="Los ingresos mensuales aumentaron un 23% de enero a marzo" />

// BIEN: botón de ícono con etiqueta accesible
<button aria-label="Eliminar elemento">
  <TrashIcon aria-hidden="true" />
</button>

Movimiento Reducido

Respeta a los usuarios que han solicitado movimiento reducido en la configuración de su sistema operativo.

export function useReducedMotion(): boolean {
  const [prefersReduced, setPrefersReduced] = useState(false);

  useEffect(() => {
    const mq = window.matchMedia('(prefers-reduced-motion: reduce)');
    setPrefersReduced(mq.matches);
    const handler = (e: MediaQueryListEvent) => setPrefersReduced(e.matches);
    mq.addEventListener('change', handler);
    return () => mq.removeEventListener('change', handler);
  }, []);

  return prefersReduced;
}

// Uso
export function AnimatedCard({ children }: { children: React.ReactNode }) {
  const reduceMotion = useReducedMotion();

  return (
    <div
      style={{
        transition: reduceMotion ? 'none' : 'transform 300ms ease'
      }}
    >
      {children}
    </div>
  );
}

Anti-Patrones

// MAL: onClick en elemento no interactivo sin soporte de teclado
<div onClick={handleClick}>Haz clic aquí</div>

// MAL: aria-label en un div que no tiene rol
<div aria-label="Navegación">...</div>

// MAL: placeholder usado como sustituto de etiqueta
<input placeholder="Ingresa tu correo electrónico" />

// MAL: tabIndex positivo crea un orden de tabulación impredecible
<button tabIndex={3}>Enviar</button>

// MAL: aria-hidden en un elemento enfocable — los usuarios de teclado quedan atrapados
<button aria-hidden="true">Abrir</button>

// MAL: role="button" en div sin controlador de teclado
<div role="button" onClick={handleClick}>Enviar</div>
// Falta: tabIndex={0}, onKeyDown para Enter/Espacio

Lista de Verificación

Antes de enviar cualquier componente interactivo a revisión:

  • Cada <input>, <select> y <textarea> tiene un <label> conectado mediante htmlFor/id
  • Los mensajes de error están vinculados con aria-describedby y marcados con role="alert"
  • No hay onClick en <div> o <span> sin role, tabIndex y onKeyDown
  • Los botones de solo ícono tienen aria-label
  • Las imágenes decorativas usan alt="" y aria-hidden="true"
  • Los modales restauran el foco al cerrarse (para un bloqueo de foco completo con desplazamiento cíclico Tab/Shift+Tab, usa una librería como focus-trap-react)
  • Las actualizaciones de contenido dinámico usan aria-live
  • Se respeta prefers-reduced-motion para las animaciones

Habilidades Relacionadas

  • frontend-patterns — patrones generales de componentes y estado en React
  • design-system — consistencia de tokens de diseño y componentes
  • motion-foundations y motion-patterns: patrones de animación con consideraciones de accesibilidad

Individual skills in this repo

This repo contains 20 individual skills — each has its own dedicated page.

accessibility

Design, implement, and audit inclusive digital products using WCAG 2.2 Level AA. Use when building or auditing UI that must meet WCAG 2.2 Level AA, or when reviewing a change for keyboard, contrast, or screen-reader support.

affaan-m/content-engine

Create platform-native content systems for X, LinkedIn, TikTok, YouTube, newsletters, and repurposed multi-platform campaigns. Use when the user wants social posts, threads, scripts, content calendars, or one source asset adapted cleanly across platforms.

affaan-m/ECC

Multi-platform content distribution across X, LinkedIn, Threads, and Bluesky. Adapts content per platform using content-engine patterns. Never posts identical content cross-platform. Use when the user wants to distribute content across social platforms.

affaan-m/fal-ai-media

Unified media generation via fal.ai MCP — image, video, and audio. Covers text-to-image (Nano Banana), text/image-to-video (Seedance, Kling, Veo 3), text-to-speech (CSM-1B), and video-to-audio (ThinkSound). Use when the user wants to generate images, videos, or audio with AI.

affaan-m/manim-video

日本語翻訳:このファイルは manim-video 用の日本語翻訳が必要です

affaan-m/remotion-video-creation

Remotion のベストプラクティス - React で動画を作成する。3D、アニメーション、音声、字幕、チャート、トランジションなどをカバーするドメイン固有の29のルール。

affaan-m/video-editing

AI-assisted video editing workflows for cutting, structuring, and augmenting real footage. Covers the full pipeline from raw capture through FFmpeg, Remotion, ElevenLabs, fal.ai, and final polish in Descript or CapCut. Use when the user wants to edit video, cut footage, create vlogs, or build video content.

agent-architecture-audit

Full-stack diagnostic for agent and LLM applications. Audits the 12-layer agent stack for wrapper regression, memory pollution, tool discipline failures, hidden repair loops, and rendering corruption. Produces severity-ranked findings with code-first fixes. Essential for developers building agent applications, autonomous loops, or any LLM-powered feature. Use when an agent or LLM feature misbehaves and the failing layer is unknown, or before shipping an agent stack.

agent-eval

Head-to-head comparison of coding agents (Claude Code, Aider, Codex, etc.) on custom tasks with pass rate, cost, time, and consistency metrics. Use when choosing between coding agents, or when a change to an agent setup needs measured pass rate, cost, and time rather than an impression.

agent-harness-construction

Design and optimize AI agent action spaces, tool definitions, and observation formatting for higher completion rates. Use when defining or revising an agent

agentic-engineering

Operate as an agentic engineer using eval-first execution, decomposition, and cost-aware model routing. Use when planning or executing engineering work that agents will carry out end to end.

agentic-os

Build persistent multi-agent operating systems on Claude Code. Covers kernel architecture, specialist agents, slash commands, file-based memory, scheduled automation, and state management without external databases. Use when building a persistent multi-agent system on Claude Code with its own memory, commands, and scheduling.

agent-introspection-debugging

Structured self-debugging workflow for AI agent failures using capture, diagnosis, contained recovery, and introspection reports. Use when an agent run fails and you need a reproducible diagnosis instead of a retry.

agent-payment-x402

Add x402 payment execution to AI agents with per-task budgets, spending controls, and non-custodial wallets. Supports Base through agentwallet-sdk and X Layer through OKX Payments / OKX Agent Payments Protocol. Use when an agent must pay for something itself and needs per-task budgets, spending controls, and a non-custodial wallet.

agent-self-evaluation

Use after completing any non-trivial task. The agent self-rates its output on 5 axes — accuracy, completeness, clarity, actionability, conciseness — with concrete evidence per criterion. Produces a structured 1-5 scorecard with specific improvement suggestions.

agent-sort

Build an evidence-backed ECC install plan for a specific repo by sorting skills, commands, rules, hooks, and extras into DAILY vs LIBRARY buckets using parallel repo-aware review passes. Use when ECC should be trimmed to what a project actually needs instead of loading the full bundle.

ai-first-engineering

Engineering operating model for teams where AI agents generate a large share of implementation output. Use when setting team process, review gates, or ownership rules for a codebase largely written by agents.

ai-regression-testing

Regression testing strategies for AI-assisted development. Sandbox-mode API testing without database dependencies, automated bug-check workflows, and patterns to catch AI blind spots where the same model writes and reviews code. Use when adding regression coverage to AI-assisted code, or when the same model both wrote and reviewed a change.

android-clean-architecture

Clean Architecture patterns for Android and Kotlin Multiplatform projects — module structure, dependency rules, UseCases, Repositories, and data layer patterns. Use when structuring modules, layers, or data flow in an Android or KMP project.

angular-developer

Generates Angular code and provides architectural guidance. Trigger when creating projects, components, or services, or for best practices on reactivity (signals, linkedSignal, resource), forms, dependency injection, routing, SSR, accessibility (ARIA), animations, styling (component styles, Tailwind CSS), testing, or CLI tooling.

Skills relacionados