Communitygithub.com

patrones-de-accesibilidad-frontend

Patrones prácticos de accesibilidad para React y Next.js que cubren formularios, HTML semántico, ARIA, navegación por teclado y gestión del foco.

¿Qué es patrones-de-accesibilidad-frontend?

Este skill proporciona una guía práctica de accesibilidad frontend para desarrolladores que trabajan con React y Next.js. Aborda los problemas más comunes detectados en revisiones de código, como la falta de etiquetas en formularios, el uso incorrecto de ARIA, elementos interactivos no semánticos y la navegación por teclado rota. Incluye ejemplos completos de código TypeScript/JSX para conectar etiquetas con campos, manejar errores con aria-describedby, implementar menús desplegables accesibles, gestionar el foco en modales y respetar la preferencia de movimiento reducido. También cubre patrones para imágenes, iconos y checklist de verificación. Está diseñado para usarse con asistentes de IA como Claude Code, Cursor o Codex, que pueden aplicar estos patrones directamente durante el desarrollo o la revisión de código.

Compatible con~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/affaan-m/everything-claude-code/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-frontend preview
patrones-de-accesibilidad-frontend preview
patrones-de-accesibilidad-frontend preview

Documentación

Patrones de Accesibilidad Frontend

Patrones prácticos de accesibilidad para React y Next.js. Cubre los problemas más comúnmente señalados en las 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 activarlo

  • Al construir o revisar componentes de formulario (<input>, <select>, <textarea>)
  • Al crear elementos interactivos (modales, 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 correspondencia htmlFor / id y los mensajes de error desconectados son los problemas más comunes señalados 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 Obligatorios

// 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 email 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: el ancla 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>Panel de control</h1>
<h4>Actividad reciente</h4>

// BIEN: niveles de encabezado secuenciales
<h1>Panel de control</h1>
<h2>Actividad reciente</h2>

Atributos ARIA

Usa ARIA solo cuando la semántica HTML nativa sea insuficiente. Un ARIA incorrecto es peor que ningún 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 al 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 accesible 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 Modales

Este ejemplo cubre el foco inicial y la restauración. Para un bloqueo completo del foco (ciclo 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 Iconos

// MAL: icono 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 icono 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="Introduce tu email" />

// 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 manejador 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 para 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 solo con icono tienen aria-label
  • Las imágenes decorativas usan alt="" y aria-hidden="true"
  • Los modales restauran el foco al cerrarse (para un bloqueo completo del foco con ciclo 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

Skills Relacionados

  • frontend-patterns — patrones generales de componentes y estado en React
  • design-system — tokens de diseño y consistencia de 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/claude-api

Anthropic Claude API patterns for Python and TypeScript. Covers Messages API, streaming, tool use, vision, extended thinking, batches, prompt caching, and Claude Agent SDK. Use when building applications with the Claude API or Anthropic SDKs.

affaan-m/everything-claude-code

End-to-end marketing campaign planning and execution. Covers audience research, positioning, campaign angle definition, landing page copy, email sequences, social posts, ad copy, short-form video scripts, and content calendars. Use as the orchestration layer for multi-channel product launches. Use when planning or executing a multi-channel product launch, or producing landing page, email, social, or ad copy.

affaan-m/everything-claude-code

Development conventions and patterns for everything-claude-code. JavaScript project with conventional commits.

affaan-m/everything-claude-code-conventions

Development conventions and patterns for everything-claude-code. JavaScript project with conventional commits.

affaan-m/frontend-design

Create distinctive, production-grade frontend interfaces with high design quality. Use when the user asks to build web components, pages, or applications and the visual direction matters as much as the code quality.

affaan-m/gget

gget CLI and Python workflow for quick genomic database queries, sequence lookup, BLAST-style searches, enrichment checks, and reproducible bioinformatics evidence logs.

affaan-m/literature-review

Systematic literature-review workflow for academic, biomedical, technical, and scientific topics, including search planning, source screening, synthesis, citation checks, and evidence logging.

affaan-m/motion-ui

Production-ready UI motion system for React/Next.js. Use when implementing animations, transitions, or motion patterns.

affaan-m/project-guidelines-example

Example project-specific skill template based on a real production application.

affaan-m/pubmed-database

Direct PubMed and NCBI E-utilities search workflows for biomedical literature, MeSH queries, PMID lookup, citation retrieval, and API-backed literature monitoring.

affaan-m/scholar-evaluation

Structured scholarly-work evaluation for papers, proposals, literature reviews, methods sections, evidence quality, citation support, and research-writing feedback.

affaan-m/uspto-database

USPTO patent and trademark data workflow for official record lookup, PatentSearch queries, TSDR checks, assignment data, and reproducible IP research logs.

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.

Skills relacionados