Communitygithub.com

frontend-a11y

Padrões práticos de acessibilidade para React e Next.js, cobrindo os problemas mais comuns em revisões de código: rótulos de formulário ausentes, uso incorreto de ARIA, elementos interativos não semânticos e navegação por teclado quebrada.

O que é frontend-a11y?

Este skill fornece padrões de acessibilidade front-end práticos e prontos para uso, direcionados especificamente para React e Next.js. Ele aborda os problemas mais frequentemente sinalizados em revisões de código, como a ausência de conexão entre rótulos e campos de formulário, uso incorreto de atributos ARIA, criação de elementos interativos não semânticos (como usar `<div>` com `onClick` em vez de `<button>`) e implementação inadequada de navegação por teclado. O skill é útil para desenvolvedores que desejam criar componentes acessíveis desde o início, economizando tempo em correções posteriores e garantindo conformidade com as melhores práticas de acessibilidade web. Ele inclui exemplos de código completos e comentados para formulários, navegação, ARIA, gerenciamento de foco, animações com `prefers-reduced-motion` e anti-padrões a evitar. O conteúdo é compatível com qualquer ambiente de desenvolvimento React, como Cursor, Codex ou Claude Code, e a formatação markdown permite fácil integração como documentação de referência rápida.

Funciona com~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/affaan-m/everything-claude-code/tree/main/skills/frontend-a11y

Perguntar na sua IA favorita

Abre um novo chat com esta habilidade de agente já pré-carregada.

Prévia

Do README da skill

frontend-a11y preview
frontend-a11y preview
frontend-a11y preview

Documentação

Padrões de Acessibilidade Front-end

Padrões práticos de acessibilidade para React e Next.js. Aborda os problemas mais comuns em revisões de código: rótulos de formulário ausentes, uso incorreto de ARIA, elementos interativos não semânticos e navegação por teclado quebrada.

Quando Ativar

  • Ao criar ou revisar componentes de formulário (<input>, <select>, <textarea>)
  • Ao criar elementos interativos (modais, dropdowns, tooltips, abas)
  • Ao usar <div> ou <span> com onClick
  • Ao adicionar atributos aria-* a qualquer elemento
  • Ao implementar navegação por teclado ou gerenciamento de foco
  • Ao receber feedback de acessibilidade de ferramentas de revisão de código (CodeRabbit, ESLint a11y)
  • Ao construir componentes que devem suportar leitores de tela

Acessibilidade de Formulários

A ausência do pareamento htmlFor/id e mensagens de erro desconectadas são os problemas mais comuns em revisões de código.

Conexão de Rótulo

// RUIM: o rótulo não tem conexão com o input — leitores de tela não conseguem associá-los
<label>Email</label>
<input type="email" />

// BOM: htmlFor corresponde ao id do input
<label htmlFor="email">Email</label>
<input id="email" type="email" />

Campos Obrigatórios

// RUIM: asterisco apenas visual não transmite nada para leitores de tela
<label htmlFor="email">Email *</label>
<input id="email" type="email" />

// BOM: required ativa a validação nativa do navegador; aria-required sinaliza para leitores de tela
<label htmlFor="email">
  Email <span aria-hidden="true">*</span>
</label>
<input id="email" type="email" required aria-required="true" />

Mensagens de Erro

// RUIM: o texto de erro existe visualmente, mas não está vinculado ao input
<input id="email" type="email" />
<span className="error">Endereço de e-mail inválido</span>

// BOM: aria-describedby conecta o input à sua mensagem de erro
// aria-invalid sinaliza o estado inválido para leitores de tela
<input
  id="email"
  type="email"
  aria-describedby="email-error"
  aria-invalid={!!error}
/>
{error && (
  <span id="email-error" role="alert">
    {error}
  </span>
)}

Formulário Acessível 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 = 'Email é obrigatório';
    if (!password) newErrors.password = 'Senha é obrigatória';
    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">
          Senha <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">Entrar</button>
    </form>
  );
}

HTML Semântico

Use o elemento que corresponde à intenção. Leitores de tela e usuários de teclado dependem de semântica nativa.

// RUIM: div não tem papel, nem suporte a teclado, nem nome acessível
<div onClick={handleClick}>Enviar</div>

// BOM: button é focalizável, ativa com Enter/Espaço, anuncia como "botão"
<button type="button" onClick={handleClick}>Enviar</button>
// RUIM: navegação não semântica
<div onClick={() => navigate('/home')}>Início</div>

// BOM: anchor suporta clique direito, clique do meio e navegação por teclado
<a href="/home">Início</a>
// RUIM: hierarquia de cabeçalhos pulada (h1 para h4)
<h1>Painel</h1>
<h4>Atividade Recente</h4>

// BOM: níveis de cabeçalho sequenciais
<h1>Painel</h1>
<h2>Atividade Recente</h2>

Atributos ARIA

Use ARIA apenas quando a semântica HTML nativa for insuficiente. ARIA errado é pior do que nenhum ARIA.

aria-label vs aria-labelledby

// aria-label: rótulo de string inline — use quando não houver texto de rótulo visível
<button aria-label="Fechar modal">
  <XIcon />
</button>

// aria-labelledby: referencia o texto de outro elemento — use quando houver um rótulo visível
<section aria-labelledby="section-title">
  <h2 id="section-title">Pedidos Recentes</h2>
  {/* conteúdo */}
</section>

aria-describedby

// Fornece descrição suplementar além do rótulo
<button
  aria-describedby="delete-warning"
  onClick={handleDelete}
> Excluir conta
</button>
<p id="delete-warning">Esta ação não pode ser desfeita.</p>

aria-live para Conteúdo Dinâmico

// Use aria-live para anunciar conteúdo que atualiza sem recarregar a página
// polite: espera o usuário terminar a ação atual antes de anunciar
// assertive: interrompe imediatamente — use apenas para erros 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 e 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>
  );
}

Navegação por Teclado

Todo elemento interativo deve ser alcançável e operável apenas pelo teclado.

Dropdown 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>
  );
}

Gerenciamento de Foco

O foco deve se mover logicamente quando o estado da IU muda — especialmente para modais e transições de rota.

Restauração de Foco em Modal

Este exemplo cobre o foco inicial e a restauração. Para um foco completo (ciclagem Tab/Shift+Tab dentro do modal), use uma biblioteca como focus-trap-react que lida com casos extremos como conteúdo dinâmico e portais aninhados.

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) {
      // Salva o elemento atualmente focalizado e move o foco para o modal
      previousFocusRef.current = document.activeElement as HTMLElement;
      modalRef.current?.focus();
    } else {
      // Restaura o foco para o elemento que abriu o 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}>Fechar</button>
    </div>
  );
}

Imagens e Ícones

// RUIM: ícone decorativo anunciado como imagem sem rótulo
<img src="/icon.svg" />

// BOM: imagem decorativa oculta dos leitores de tela
<img src="/decoration.png" alt="" aria-hidden="true" />

// BOM: imagem significativa com texto alternativo descritivo
<img src="/chart.png" alt="A receita mensal aumentou 23% de janeiro a março" />

// BOM: botão de ícone com rótulo acessível
<button aria-label="Excluir item">
  <TrashIcon aria-hidden="true" />
</button>

Movimento Reduzido

Respeite os usuários que solicitaram movimento reduzido nas configurações do sistema operacional.

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-Padrões

// RUIM: onClick em elemento não interativo sem suporte a teclado
<div onClick={handleClick}>Clique em mim</div>

// RUIM: aria-label em uma div que não tem papel
<div aria-label="Navegação">...</div>

// RUIM: placeholder usado como substituto do rótulo
<input placeholder="Digite seu email" />

// RUIM: tabIndex positivo cria ordem de tabulação imprevisível
<button tabIndex={3}>Enviar</button>

// RUIM: aria-hidden em um elemento focalizável — usuários de teclado ficam presos
<button aria-hidden="true">Abrir</button>

// RUIM: role="button" em div sem manipulador de teclado
<div role="button" onClick={handleClick}>Enviar</div>
// Faltando: tabIndex={0}, onKeyDown para Enter/Espaço

Lista de Verificação

Antes de enviar qualquer componente interativo para revisão:

  • Cada <input>, <select> e <textarea> tem um <label> conectado via htmlFor/id
  • Mensagens de erro estão vinculadas com aria-describedby e marcadas com role="alert"
  • Nenhum onClick em <div> ou <span> sem role, tabIndex e onKeyDown
  • Botões apenas com ícone possuem aria-label
  • Imagens decorativas usam alt="" e aria-hidden="true"
  • Modais restauram o foco ao fechar (para foco completo com ciclagem Tab/Shift+Tab, use uma biblioteca como focus-trap-react)
  • Atualizações de conteúdo dinâmico usam aria-live
  • prefers-reduced-motion é respeitado para animações

Skills Relacionados

  • frontend-patterns — padrões gerais de componentes e estado em React
  • design-system — consistência de tokens de design e componentes
  • motion-foundations e motion-patterns: padrões de animação com considerações de acessibilidade

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.

Habilidades Relacionadas