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.
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
| Caso | Por que Radix UI ajuda | O que Claude Code conecta |
|---|---|---|
| Confirmação de exclusão ou cancelamento | Foco, título e descrição ficam confiáveis | API, pending, erros e testes |
| Menu de conta | Navegação por teclado, separadores e radio items | Usuário, logout, billing e analytics |
| Tabs de configurações | Relação tablist, tab e panel permanece correta | Formulá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.
Links oficiais e checklist
- Radix Primitives Introduction
- Radix Dialog docs
- Radix Dropdown Menu docs
- Radix Tabs docs
- WAI-ARIA Modal Dialog Pattern
- MDN dialog role
- Claude Code getting started
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.
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.
Sobre o autor
Masa
Engenheiro focado em workflows práticos com Claude Code.
Artigos relacionados
Workflow Obsidian para CLAUDE.md com Claude Code
Transforme notas de trabalho do Obsidian em notas operacionais CLAUDE.md para preservar contexto.
Claude Code Revenue CTA Routing: artigos para PDF, Gumroad e consultoria
Um fluxo com Claude Code para levar leitores ao PDF grátis, Gumroad ou consultoria conforme intenção.
Regras de handoff para equipes com Claude Code: evidências, permissões, rollback e receita
Formato prático para entregar trabalho do Claude Code com prova, permissões, rollback, PDF grátis, Gumroad e consultoria.