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.
Artigos relacionados
Player de vídeo em React: legendas, velocidade e métricas de visualização
Crie um player de vídeo para cursos e mídia com o Claude Code: reprodução, legendas, acessibilidade e métricas, com código React real.
shadcn/ui com Claude Code: guia prático para React
Use Claude Code com shadcn/ui: instalação, Button/Card/Form/Dialog/Table, tokens e testes visuais com Playwright.
Criar uma command palette com Claude Code
Implemente uma command palette com Claude Code: atalhos, ARIA, busca, riscos, testes e CTA.
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.