Use Cases (Atualizado: 02/06/2026)

React acessível com Claude Code e Radix UI

Implemente Dialog, Dropdown e Tabs com Claude Code e Radix UI: instalação, exemplos React, estilos, armadilhas e checklist.

React acessível com Claude Code e Radix UI

Criar um modal, um menu dropdown ou abas em React parece simples quando você testa apenas com o mouse. O problema aparece no teclado: o foco não entra no diálogo, Escape não fecha, o leitor de tela não anuncia o título, o foco volta para o lugar errado ou o menu quebra em telas pequenas.

Radix UI é útil porque entrega primitives sem estilo. Você continua decidindo cores, espaçamento, tipografia e animações, mas reaproveita comportamento sólido para Dialog, Dropdown Menu e Tabs. Com Claude Code, essa separação reduz bastante a chance de gerar uma implementação frágil.

Em vez de pedir “crie um modal bonito”, peça “use Radix UI, preserve acessibilidade e adapte o visual ao projeto”. Se quiser uma camada mais opinativa baseada em Radix, veja o guia shadcn/ui com Claude Code. Para uma revisão mais ampla, leia também o guia de acessibilidade com Claude Code.

Por que Radix UI combina com Claude Code

A documentação oficial apresenta Radix Primitives como uma biblioteca de componentes de baixo nível focada em acessibilidade, personalização e experiência de desenvolvimento. Ela não tenta impor uma aparência. Ela fornece papéis, gerenciamento de foco, navegação por teclado e estrutura de componentes.

Radix Dialog, por exemplo, suporta modo modal e não modal, mantém o foco dentro do modal, fecha com Escape e usa Title e Description para ajudar leitores de tela. Radix Tabs segue o padrão WAI-ARIA de abas e lida com setas, Home e End. Dropdown Menu oferece labels, separadores, radio items e submenus sem escrever todo o sistema de foco manualmente.

Claude Code trabalha em um ciclo de entender contexto, editar e verificar. Se você pedir para ele recriar tudo com div, o diff fica grande e difícil de revisar. Quando Radix UI é a base, Claude Code pode focar no que é específico do seu repositório: estado React, chamadas API, classes CSS, eventos de analytics, testes e checklist.

flowchart LR
  A["Descrever o requisito para Claude Code"] --> B["Usar Radix UI para o comportamento"]
  B --> C["Conectar estado React e lógica do produto"]
  C --> D["Aplicar CSS ou tokens do projeto"]
  D --> E["Verificar teclado, leitor de tela e mobile"]

Instalação

Radix também documenta o pacote unificado radix-ui, mas muitos projetos React usam pacotes individuais. Os exemplos abaixo usam pacotes individuais para deixar claro no package.json quais primitives foram adicionadas.

npm install @radix-ui/react-dialog @radix-ui/react-dropdown-menu @radix-ui/react-tabs

Com pnpm:

pnpm add @radix-ui/react-dialog @radix-ui/react-dropdown-menu @radix-ui/react-tabs

Se Claude Code ainda não estiver instalado, confira a documentação oficial. A instalação via npm é:

npm install -g @anthropic-ai/claude-code

Em equipe, defina também permissões, atualização, comandos permitidos e responsabilidade de revisão. Instalar a ferramenta não substitui governança de código.

Prompt recomendado para Claude Code

Um bom prompt descreve o contrato de interação, não só a aparência.

claude "Adicione à tela React + TypeScript existente um Dialog de confirmação, um Dropdown Menu de usuário e Tabs de configurações.
Condições:
- usar @radix-ui/react-dialog, @radix-ui/react-dropdown-menu e @radix-ui/react-tabs
- manter Dialog.Title e Dialog.Description
- botões de fechar só com ícone devem ter aria-label
- não remover foco visível; usar :focus-visible
- reutilizar design tokens existentes quando houver
- no final listar checks de teclado, mobile, typecheck e acessibilidade"

Depois da alteração, revise o diff. Veja se asChild não criou botão dentro de botão, se Dialog.Title não foi removido por estética e se o CSS não apagou o foco.

Exemplo React copiável

O exemplo abaixo contém um diálogo de confirmação, um menu de usuário e abas de configuração. Funciona em Vite, React SPA ou Next.js Client Component. No App Router do Next.js, adicione "use client"; no topo.

import * as React from "react";
import * as Dialog from "@radix-ui/react-dialog";
import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
import * as Tabs from "@radix-ui/react-tabs";
import "./radix-accessible-demo.css";

type User = { name: string; email: string };

type ConfirmDialogProps = {
  open: boolean;
  onOpenChange: (open: boolean) => void;
  title: string;
  description: string;
  confirmLabel?: string;
  danger?: boolean;
  onConfirm: () => Promise<void> | void;
};

export function ConfirmDialog({
  open,
  onOpenChange,
  title,
  description,
  confirmLabel = "Confirm",
  danger = false,
  onConfirm,
}: ConfirmDialogProps) {
  const [pending, setPending] = React.useState(false);

  async function handleConfirm() {
    setPending(true);
    try {
      await onConfirm();
      onOpenChange(false);
    } finally {
      setPending(false);
    }
  }

  return (
    <Dialog.Root open={open} onOpenChange={onOpenChange}>
      <Dialog.Portal>
        <Dialog.Overlay className="radix-overlay" />
        <Dialog.Content className="radix-dialog">
          <Dialog.Title className="radix-dialog-title">{title}</Dialog.Title>
          <Dialog.Description className="radix-dialog-description">
            {description}
          </Dialog.Description>
          <div className="button-row">
            <Dialog.Close asChild>
              <button type="button" className="button secondary">Cancel</button>
            </Dialog.Close>
            <button
              type="button"
              className={`button ${danger ? "danger" : "primary"}`}
              disabled={pending}
              onClick={handleConfirm}
            >
              {pending ? "Working..." : confirmLabel}
            </button>
          </div>
          <Dialog.Close asChild>
            <button type="button" className="icon-button" aria-label="Close dialog">
              x
            </button>
          </Dialog.Close>
        </Dialog.Content>
      </Dialog.Portal>
    </Dialog.Root>
  );
}

export function UserMenu({
  user,
  onOpenProfile,
  onOpenBilling,
  onSignOut,
}: {
  user: User;
  onOpenProfile: () => void;
  onOpenBilling: () => void;
  onSignOut: () => void;
}) {
  const [theme, setTheme] = React.useState<"light" | "dark" | "system">("system");

  return (
    <DropdownMenu.Root>
      <DropdownMenu.Trigger asChild>
        <button type="button" className="user-trigger" aria-label={`${user.name} menu`}>
          <span className="avatar" aria-hidden="true">{user.name.slice(0, 1)}</span>
          <span>{user.name}</span>
        </button>
      </DropdownMenu.Trigger>
      <DropdownMenu.Portal>
        <DropdownMenu.Content className="dropdown-content" align="end" sideOffset={8}>
          <DropdownMenu.Label className="dropdown-label">{user.email}</DropdownMenu.Label>
          <DropdownMenu.Separator className="dropdown-separator" />
          <DropdownMenu.Item className="dropdown-item" onSelect={() => onOpenProfile()}>
            Profile
          </DropdownMenu.Item>
          <DropdownMenu.Item className="dropdown-item" onSelect={() => onOpenBilling()}>
            Billing
          </DropdownMenu.Item>
          <DropdownMenu.Separator className="dropdown-separator" />
          <DropdownMenu.RadioGroup
            value={theme}
            onValueChange={(value) => setTheme(value as "light" | "dark" | "system")}
          >
            <DropdownMenu.RadioItem className="dropdown-item" value="light">Light</DropdownMenu.RadioItem>
            <DropdownMenu.RadioItem className="dropdown-item" value="dark">Dark</DropdownMenu.RadioItem>
            <DropdownMenu.RadioItem className="dropdown-item" value="system">System</DropdownMenu.RadioItem>
          </DropdownMenu.RadioGroup>
          <DropdownMenu.Separator className="dropdown-separator" />
          <DropdownMenu.Item className="dropdown-item danger-text" onSelect={() => onSignOut()}>
            Sign out
          </DropdownMenu.Item>
        </DropdownMenu.Content>
      </DropdownMenu.Portal>
    </DropdownMenu.Root>
  );
}

export function SettingsTabs() {
  return (
    <Tabs.Root defaultValue="profile" className="tabs-root">
      <Tabs.List className="tabs-list" aria-label="Account settings">
        <Tabs.Trigger className="tabs-trigger" value="profile">Profile</Tabs.Trigger>
        <Tabs.Trigger className="tabs-trigger" value="security">Security</Tabs.Trigger>
        <Tabs.Trigger className="tabs-trigger" value="notifications">Notifications</Tabs.Trigger>
      </Tabs.List>
      <Tabs.Content className="tabs-content" value="profile">
        <label className="field">
          <span>Display name</span>
          <input defaultValue="Masa" />
        </label>
      </Tabs.Content>
      <Tabs.Content className="tabs-content" value="security">
        <p>Require two-factor authentication before changing billing settings.</p>
        <button type="button" className="button secondary">Review security</button>
      </Tabs.Content>
      <Tabs.Content className="tabs-content" value="notifications">
        <label className="check-row">
          <input type="checkbox" defaultChecked />
          <span>Email me when a project is exported.</span>
        </label>
      </Tabs.Content>
    </Tabs.Root>
  );
}

Notas de estilo

Radix UI não vem com CSS. Você pode usar Tailwind, CSS Modules, CSS comum ou tokens. O essencial é manter foco visível, limitar o tamanho do Dialog no mobile e deixar estados ativos claros.

.radix-overlay {
  position: fixed;
  inset: 0;
  background: rgba(15, 23, 42, 0.55);
}

.radix-dialog {
  position: fixed;
  left: 50%;
  top: 50%;
  width: min(calc(100vw - 32px), 480px);
  max-height: calc(100vh - 32px);
  overflow: auto;
  transform: translate(-50%, -50%);
  border-radius: 8px;
  background: white;
  box-shadow: 0 24px 80px rgba(15, 23, 42, 0.28);
  padding: 24px;
}

.button-row {
  display: flex;
  justify-content: flex-end;
  gap: 12px;
  margin-top: 24px;
}

.dropdown-content {
  min-width: 220px;
  border: 1px solid #e2e8f0;
  border-radius: 8px;
  background: white;
  box-shadow: 0 18px 50px rgba(15, 23, 42, 0.18);
  padding: 6px;
}

.dropdown-item {
  border-radius: 6px;
  cursor: pointer;
  outline: none;
  padding: 8px 10px;
}

.dropdown-item[data-highlighted] {
  background: #eff6ff;
  color: #1d4ed8;
}

.tabs-list {
  display: flex;
  border-bottom: 1px solid #e2e8f0;
  gap: 4px;
}

.tabs-trigger {
  border: 0;
  border-bottom: 2px solid transparent;
  background: transparent;
  cursor: pointer;
  padding: 10px 12px;
}

.tabs-trigger[data-state="active"] {
  border-color: #2563eb;
  color: #1d4ed8;
  font-weight: 700;
}

:focus-visible {
  outline: 3px solid #f59e0b;
  outline-offset: 2px;
}

Três casos de uso

CasoPor que Radix UI ajudaO que Claude Code conecta
Confirmação de exclusão ou cancelamentoFoco, título e descrição ficam confiáveisAPI, pending, erros e testes
Menu de contaNavegação por teclado, separadores e radio itemsUsuário, logout, billing e analytics
Tabs de configuraçõesRelação tablist, tab e panel permanece corretaFormulários, save, URL e dirty state

O primeiro caso é painel SaaS. Excluir projeto, cancelar plano ou mudar permissão precisa de clareza. Radix Dialog entrega o comportamento, e Claude Code conecta copy, API e estados.

O segundo caso é site de curso, mídia ou assinatura. O menu de conta reúne perfil, downloads, histórico de compra e logout. Um menu que só funciona com clique falha para usuários de teclado.

O terceiro caso é página de configurações. Tabs separa perfil, segurança e notificações, mas cada área precisa ter responsabilidade de salvamento clara. Coloque isso no prompt.

Armadilhas comuns

Não remova Dialog.Title por estética. Se o título não deve aparecer, mantenha uma versão visualmente oculta para leitores de tela. A documentação MDN de dialog reforça a necessidade de nome e foco correto.

Não esconda o foco. Trocar o outline por um estilo de marca é aceitável; deixar invisível não é.

Revise asChild. Um button dentro de outro button é HTML inválido e pode ter comportamento inconsistente.

Evite empilhar modais sem necessidade. O padrão WAI-ARIA de Modal Dialog espera que o foco entre no diálogo e volte ao disparador ao fechar.

Cheque mobile. Dialog fixo de 600px, menu cortado à direita ou labels longos em Tabs quebram uma tela que parecia pronta no desktop.

Antes de publicar, abra e feche o Dialog só com teclado, confirme Escape, retorno de foco, navegação do Dropdown por setas, troca de Tabs por setas e layout mobile sem overflow.

CTA de monetização

Esse assunto costuma converter porque o leitor tem um projeto real. A oferta natural não é “compre uma biblioteca”, mas “traga seu repositório e deixe a UI revisável e acessível”.

ClaudeCodeLab pode ajudar com treinamento e consultoria Claude Code: CLAUDE.md, regras de componentes, checks de acessibilidade, prompts de revisão e scripts de verificação. Para solo builders, templates e checklists bastam para começar; para equipes, uma revisão em tela real gera mais valor.

Resultado testado

Em uma tela React de teste da Masa, trocar um modal manual por Radix Dialog e mover menu e tabs para Radix aumentou um pouco o código, mas deixou a revisão mais clara. Pedir ao Claude Code para revisar retorno de foco, nomes para leitores de tela, largura mobile e teclado trouxe feedback melhor do que pedir apenas “melhore o visual”. Radix UI não dispensa pensar em acessibilidade; ele dá ao Claude Code uma base mais segura.

#Claude Code #Radix UI #React #acessibilidade #componentes UI
Grátis

PDF grátis: cheatsheet do Claude Code

Informe seu e-mail e baixe uma página com comandos, hábitos de revisão e workflows seguros.

Cuidamos dos seus dados e não enviamos spam.

Masa

Sobre o autor

Masa

Engenheiro focado em workflows práticos com Claude Code.