Use Cases (Atualizado: 02/06/2026)

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.

shadcn/ui com Claude Code: guia prático para React

shadcn/ui não é uma biblioteca tradicional que fica escondida em node_modules. A CLI copia o código dos componentes para dentro do seu projeto, normalmente em src/components/ui. Isso dá controle total sobre Button, Card, Dialog e Table, mas também faz com que esses arquivos sejam responsabilidade da equipe.

Esse modelo combina bem com Claude Code. Ele consegue ler os componentes gerados, entender as classes Tailwind, ajustar variantes, conectar formulários e adicionar testes Playwright. O risco aparece quando o pedido é amplo demais: “crie um dashboard” pode virar botões duplicados, configuração Tailwind antiga ou um Dialog com acessibilidade quebrada.

Este guia mostra um fluxo seguro para Vite + React + TypeScript: instalar shadcn/ui, adicionar Button/Card/Form/Dialog/Table, organizar tokens de design, evitar deriva por copiar e colar e validar a interface com Playwright. Se você ainda está começando com Claude Code, leia antes o guia de introdução ao Claude Code.

Comece pelas fontes oficiais

Antes de editar, informe ao Claude Code quais documentos usar como referência. A documentação de Vite do shadcn/ui usa shadcn@latest; Tailwind CSS v4 explica variáveis de tema com @theme; Radix UI documenta o comportamento acessível de Dialog; e Playwright usa toHaveScreenshot() para comparação visual.

flowchart LR
  A["Claude Code inspeciona o projeto"] --> B["Inicializar shadcn/ui"]
  B --> C["Adicionar componentes"]
  C --> D["Centralizar tokens"]
  D --> E["Criar camada app"]
  E --> F["Validar com Playwright"]

Instalação e inicialização

O exemplo usa Vite para manter o escopo claro. Em Next.js a ideia é parecida, mas iniciantes devem evitar misturar UI, rotas e Server Components no primeiro passo.

pnpm create vite@latest shadcn-claude-demo -- --template react-ts
cd shadcn-claude-demo
pnpm install
pnpm add tailwindcss @tailwindcss/vite
pnpm add -D @types/node

Configure Tailwind e o alias @ em vite.config.ts.

import path from "node:path"
import react from "@vitejs/plugin-react"
import tailwindcss from "@tailwindcss/vite"
import { defineConfig } from "vite"

export default defineConfig({
  plugins: [react(), tailwindcss()],
  resolve: {
    alias: {
      "@": path.resolve(__dirname, "./src"),
    },
  },
})

Repita o alias no TypeScript.

{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"]
    }
  }
}

Peça ao Claude Code para inspecionar primeiro.

Quero adicionar shadcn/ui a este projeto Vite + React + TypeScript.
Primeiro leia package.json, vite.config.ts, tsconfig*.json e src/index.css.
Informe apenas o que está faltando. Depois da minha aprovação,
execute shadcn@latest init e adicione os componentes necessários.

Depois execute:

pnpm dlx shadcn@latest init
pnpm dlx shadcn@latest add button card field input label dialog table
pnpm add react-hook-form zod @hookform/resolvers

A documentação atual de formulários do shadcn/ui usa React Hook Form, Zod e Field. Evite colar exemplos antigos sem verificar se ainda combinam com a versão do projeto.

Button e Card primeiro

Comece com um componente pequeno. Deixe os arquivos gerados por shadcn/ui em src/components/ui e coloque a UI específica do produto em src/components/app.

import { Button } from "@/components/ui/button"
import {
  Card,
  CardContent,
  CardDescription,
  CardFooter,
  CardHeader,
  CardTitle,
} from "@/components/ui/card"

type ProjectSummaryCardProps = {
  name: string
  openIssues: number
  onCreateTask: () => void
}

export function ProjectSummaryCard({
  name,
  openIssues,
  onCreateTask,
}: ProjectSummaryCardProps) {
  return (
    <Card className="max-w-md">
      <CardHeader>
        <CardTitle>{name}</CardTitle>
        <CardDescription>
          Review open UI issues before starting the next task.
        </CardDescription>
      </CardHeader>
      <CardContent>
        <p className="text-3xl font-semibold">{openIssues}</p>
        <p className="text-muted-foreground text-sm">open issues</p>
      </CardContent>
      <CardFooter>
        <Button onClick={onCreateTask}>Add task</Button>
      </CardFooter>
    </Card>
  )
}

Use um pedido de revisão estreito.

Revise apenas src/components/app/ProjectSummaryCard.tsx.
Verifique tipos de props, imports de shadcn/ui, legibilidade das classes Tailwind e acessibilidade.
Sugira mudanças em outros arquivos somente se forem necessárias, explicando o motivo.

Form com Field, React Hook Form e Zod

Um formulário real precisa de validação, mensagens de erro, aria-invalid, botão desabilitado durante envio, autocomplete e espaço para erros do servidor.

"use client"

import { zodResolver } from "@hookform/resolvers/zod"
import { Controller, useForm } from "react-hook-form"
import * as z from "zod"

import { Button } from "@/components/ui/button"
import {
  Field,
  FieldDescription,
  FieldError,
  FieldGroup,
  FieldLabel,
} from "@/components/ui/field"
import { Input } from "@/components/ui/input"

const contactSchema = z.object({
  email: z.string().email("Enter a valid email address"),
  topic: z.string().min(4, "Use at least 4 characters"),
})

type ContactFormValues = z.infer<typeof contactSchema>

export function ContactForm() {
  const form = useForm<ContactFormValues>({
    resolver: zodResolver(contactSchema),
    defaultValues: {
      email: "",
      topic: "",
    },
  })

  function onSubmit(values: ContactFormValues) {
    console.log("submit", values)
  }

  return (
    <form className="max-w-md space-y-4" onSubmit={form.handleSubmit(onSubmit)}>
      <FieldGroup>
        <Controller
          name="email"
          control={form.control}
          render={({ field, fieldState }) => (
            <Field data-invalid={fieldState.invalid}>
              <FieldLabel htmlFor={field.name}>Email</FieldLabel>
              <Input
                {...field}
                id={field.name}
                type="email"
                aria-invalid={fieldState.invalid}
                autoComplete="email"
              />
              <FieldDescription>We will use this address to reply.</FieldDescription>
              {fieldState.invalid && <FieldError errors={[fieldState.error]} />}
            </Field>
          )}
        />
      </FieldGroup>

      <Button type="submit" disabled={form.formState.isSubmitting}>
        Send
      </Button>
    </form>
  )
}

Formulários pequenos podem manter schema e componente juntos. Quando crescerem, peça uma separação explícita entre schema.ts, ContactForm.tsx e action de servidor.

Dialog e Table com segurança

Dialog não é apenas um overlay. Radix UI cuida de modo modal, foco, fechamento com Escape e anúncios para leitores de tela por meio de Title e Description. shadcn/ui estiliza essas partes, mas elas devem continuar no markup.

import { useState } from "react"

import { Button } from "@/components/ui/button"
import {
  Dialog,
  DialogContent,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogTitle,
} from "@/components/ui/dialog"
import {
  Table,
  TableBody,
  TableCell,
  TableHead,
  TableHeader,
  TableRow,
} from "@/components/ui/table"

type Customer = {
  id: string
  name: string
  plan: "Free" | "Pro" | "Team"
}

const customers: Customer[] = [
  { id: "cus_001", name: "Aoi Tanaka", plan: "Pro" },
  { id: "cus_002", name: "Mika Sato", plan: "Team" },
]

export function CustomerTable() {
  const [selectedCustomer, setSelectedCustomer] = useState<Customer | null>(null)

  return (
    <>
      <Table>
        <TableHeader>
          <TableRow>
            <TableHead>Customer</TableHead>
            <TableHead>Plan</TableHead>
            <TableHead className="text-right">Actions</TableHead>
          </TableRow>
        </TableHeader>
        <TableBody>
          {customers.map((customer) => (
            <TableRow key={customer.id}>
              <TableCell className="font-medium">{customer.name}</TableCell>
              <TableCell>{customer.plan}</TableCell>
              <TableCell className="text-right">
                <Button
                  variant="outline"
                  size="sm"
                  onClick={() => setSelectedCustomer(customer)}
                >
                  Edit
                </Button>
              </TableCell>
            </TableRow>
          ))}
        </TableBody>
      </Table>

      <Dialog open={selectedCustomer !== null} onOpenChange={() => setSelectedCustomer(null)}>
        <DialogContent>
          <DialogHeader>
            <DialogTitle>Edit customer</DialogTitle>
            <DialogDescription>
              Review the contract plan for {selectedCustomer?.name}.
            </DialogDescription>
          </DialogHeader>
          <DialogFooter>
            <Button variant="outline" onClick={() => setSelectedCustomer(null)}>
              Close
            </Button>
          </DialogFooter>
        </DialogContent>
      </Dialog>
    </>
  )
}

Em uma app real, carregue os dados na página e passe customers e onEdit para o componente. Isso reduz a chance de Claude Code mexer na camada de dados durante um ajuste visual.

Tokens de design e Tailwind

Tokens de design são valores nomeados para cor, raio, sombra, fonte e espaçamento. No Tailwind v4, @theme influencia as utilities geradas. Componentes shadcn/ui também usam variáveis CSS como --background, --primary e --border.

@import "tailwindcss";

@theme {
  --font-sans: Inter, system-ui, sans-serif;
  --radius-card: 0.75rem;
  --color-brand-500: oklch(0.62 0.18 250);
}

:root {
  --radius: 0.625rem;
  --background: oklch(1 0 0);
  --foreground: oklch(0.145 0 0);
  --primary: oklch(0.205 0 0);
  --primary-foreground: oklch(0.985 0 0);
  --border: oklch(0.922 0 0);
  --input: oklch(0.922 0 0);
  --ring: oklch(0.708 0 0);
}

.dark {
  --background: oklch(0.145 0 0);
  --foreground: oklch(0.985 0 0);
  --primary: oklch(0.922 0 0);
  --primary-foreground: oklch(0.205 0 0);
  --border: oklch(1 0 0 / 10%);
  --input: oklch(1 0 0 / 15%);
  --ring: oklch(0.556 0 0);
}

Se o projeto ainda usa Tailwind v3, peça dois planos: migração para v4 e correção mínima compatível com v3. Misturar os dois costuma gerar classes que não aplicam e tokens duplicados.

Evitar deriva por copiar e colar

shadcn/ui dá o código fonte. Sem regras, a equipe cria variações quase iguais de Button e Card.

RegraMotivo
src/components/ui fica para primitivas shadcn/uiDiferenças são fáceis de rastrear
UI de produto vai para src/components/appA base não recebe lógica de negócio
Não mudar aliases em components.json sem motivoImports ficam estáveis
Informar arquivos alvo ao Claude CodeDiminui duplicação
Os arquivos em src/components/ui vêm de shadcn/ui.
Não crie um novo Button ou Card.
Use imports existentes e coloque UI específica em src/components/app.
Depois execute rg "function .*Button|export .*Button" src/components
e informe se surgiram duplicatas.
git diff -- src/components/ui
git diff -- src/components/app
pnpm lint
pnpm build

Para melhorar o fluxo diário, veja também as dicas de produtividade com Claude Code.

Testes visuais com Playwright

Playwright cria uma captura base e compara execuções futuras. O sistema, navegador, fontes e viewport devem ser próximos do ambiente de CI.

pnpm create playwright
pnpm playwright install
pnpm dev
pnpm playwright test
import { expect, test } from "@playwright/test"

test("customer table dialog visual state", async ({ page }) => {
  await page.goto("/customers")
  await page.getByRole("button", { name: "Edit" }).first().click()

  await expect(page).toHaveScreenshot("customers-dialog.png", {
    maxDiffPixels: 120,
  })
})

Atualize snapshots apenas quando a alteração visual for intencional.

pnpm playwright test --update-snapshots

Ao falhar, dê um problema específico ao Claude Code.

O teste customers-dialog.png do Playwright falhou.
No mobile, os botões do footer do Dialog estão muito próximos.
Leia apenas src/components/app/CustomerTable.tsx e CSS relacionado.
Corrija o espaçamento sem remover DialogTitle ou DialogDescription.

Três casos de uso

Primeiro, criar um novo painel administrativo. Button, Card, Table e Dialog cobrem lista, detalhe, edição e confirmação. Peça UI estática primeiro e API depois.

Segundo, padronizar um produto existente. Troque um formulário de busca, um Dialog de configurações ou um Card de estado vazio antes de tentar redesenhar tudo.

Terceiro, protótipos pagos ou demos para clientes. Dados mock são aceitáveis, mas as fronteiras dos componentes devem parecer produção para que o código possa evoluir.

Acessibilidade também se encaixa bem. Combine este fluxo com o guia de acessibilidade com Claude Code para revisar labels, teclado, estrutura de Dialog e mensagens de erro.

Armadilhas comuns

A primeira armadilha é colar configuração antiga de Tailwind em projeto v4. Confira versão e pipeline antes de alterar.

A segunda é colocar lógica de negócio em components/ui. Uma variante de Button para plano pago deve ficar em um wrapper da camada app.

A terceira é remover DialogTitle porque ele atrapalha visualmente. Se não deve aparecer, esconda de forma deliberada sem apagar a informação.

A quarta é depender apenas de required. Formulários reais precisam de erros traduzidos, validação assíncrona e erros de servidor.

A quinta é gerar snapshots Playwright fora do ambiente usado no CI. Fontes e viewport também fazem diferença.

Prompt pronto e resultado testado

Objetivo: adicionar shadcn/ui a uma tela admin Vite + React + TypeScript.
Usar Button, Card, Form baseado em Field, Dialog e Table.

Restrições:
- Seguir a documentação oficial atual
- Manter src/components/ui para primitivas shadcn/ui
- Colocar componentes da app em src/components/app
- Usar React Hook Form + Zod + Field
- Preservar DialogTitle e DialogDescription
- Explicar alterações de Tailwind v4 @theme e variáveis CSS
- Adicionar um teste Playwright toHaveScreenshot

Processo:
1. Inspecionar a configuração existente
2. Propor plano de alteração
3. Implementar após aprovação
4. Executar pnpm build e pnpm playwright test
5. Resumir arquivos alterados e riscos

Resultado testado: em uma app Vite mínima, esse fluxo reproduziu a instalação dos componentes, a adição de uma cor de marca com @theme, um formulário com Field, um Dialog aberto a partir de uma linha da Table e uma checagem visual com Playwright. A única segunda rodada foi no render de erros do formulário; pedir explicitamente aria-invalid e FieldError deixou o diff claro.

Para times, coloque o prompt em CLAUDE.md. A ClaudeCodeLab também oferece templates pagos com regras de UI, checklists de revisão e prompts de Claude Code para reduzir explicações repetidas a cada sprint.

#Claude Code #shadcn/ui #React #Tailwind CSS #Biblioteca 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.