Communitygithub.com

frontend-a11y

Padrões práticos de acessibilidade para React e Next.js, abordando problemas comuns em revisões de código.

O que é frontend-a11y?

Este skill fornece um conjunto abrangente de padrões de acessibilidade para componentes React e Next.js, com foco em correções práticas para problemas comuns identificados em revisões de código. Ele cobre tópicos essenciais como conexão de rótulos de formulário, elementos interativos não semânticos, uso correto de ARIA, navegação por teclado e gerenciamento de foco. O skill inclui exemplos de código detalhados mostrando más práticas e boas práticas, desde campos de formulário simples até menus dropdown personalizados e modais com foco restaurado. Também aborda tópicos como imagens, ícones, animação reduzida e anti-padrões. Útil para revisões de código, construção de componentes acessíveis e melhoria geral da usabilidade para leitores de tela e navegação por teclado.

Funciona com~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/affaan-m/ECC/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 Frontend

Padrões práticos de acessibilidade para React e Next.js. Aborda os problemas mais comumente sinalizados 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 construir ou revisar componentes de formulário (<input>, <select>, <textarea>)
  • Ao criar elementos interativos (modais, menus dropdown, dicas de ferramentas, 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 falta de pareamento htmlFor / id e mensagens de erro desconectadas são os problemas mais comuns sinalizados 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 email 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 da semântica nativa.

// RUIM: div não tem papel, suporte a teclado ou 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: âncora suporta clique direito, clique do meio e navegação por teclado
<a href="/home">Início</a>
// RUIM: hierarquia de títulos pulada (h1 para h4)
<h1>Painel</h1>
<h4>Atividade Recente</h4>

// BOM: níveis de título sequenciais
<h1>Painel</h1>
<h2>Atividade Recente</h2>

Atributos ARIA

Use ARIA apenas quando a semântica nativa do HTML for insuficiente. ARIA errada é pior do que nenhuma 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 é atualizado sem recarregar a página
// polite: aguarda 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.

Menu 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 interface do usuário 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 uma armadilha de foco completa (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 dentro do 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 alt 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 de 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 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:

  • Todo <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 têm aria-label
  • Imagens decorativas usam alt="" e aria-hidden="true"
  • Modais restauram o foco ao fechar (para armadilha de foco completa 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 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/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.

Habilidades Relacionadas