shadcn/ui con Claude Code: guía práctica para React
Aprende a usar Claude Code con shadcn/ui: instalación, Button/Card/Form/Dialog/Table, tokens y pruebas visuales.
shadcn/ui no funciona como una biblioteca clásica que importas desde node_modules y nunca vuelves a mirar. Su CLI copia el código de los componentes dentro de tu proyecto, normalmente en src/components/ui. Eso te da libertad para adaptar Button, Card, Dialog o Table, pero también te obliga a mantener una pequeña capa de diseño como si fuera código propio.
Claude Code encaja muy bien en ese modelo porque puede leer esos archivos, entender tus clases de Tailwind, cambiar variantes, conectar formularios y añadir pruebas. El problema aparece cuando se le pide algo demasiado amplio, como “hazme un dashboard”: puede crear botones duplicados, mezclar configuración antigua de Tailwind o eliminar piezas importantes de accesibilidad en Dialog.
Esta guía muestra un flujo seguro para una app Vite + React + TypeScript: instalar shadcn/ui, añadir Button/Card/Form/Dialog/Table, organizar tokens de diseño, evitar deriva por copiar y pegar, y comprobar la interfaz con Playwright. Si todavía no dominas Claude Code, empieza por la guía de inicio de Claude Code.
Empieza por fuentes oficiales
Antes de editar, conviene darle a Claude Code las fuentes actuales. La documentación de Vite para shadcn/ui usa shadcn@latest; Tailwind CSS v4 documenta variables de tema con @theme; Radix UI explica el comportamiento accesible de Dialog; y Playwright usa toHaveScreenshot() para comparaciones visuales.
- shadcn/ui Vite installation
- shadcn/ui React Hook Form guide
- Tailwind CSS theme variables
- Radix UI Dialog docs
- Claude Code quickstart
- Playwright visual comparisons
flowchart LR
A["Claude Code inspecciona el proyecto"] --> B["Inicializar shadcn/ui"]
B --> C["Añadir componentes base"]
C --> D["Centralizar tokens"]
D --> E["Crear componentes de app"]
E --> F["Validar con Playwright"]
Instalación e inicialización
El ejemplo usa Vite para reducir ruido. En Next.js el enfoque es parecido, pero para principiantes es mejor no mezclar UI, routing y Server Components en el primer paso.
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
Configura Tailwind y el alias @ en 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"),
},
},
})
Repite el alias en TypeScript para que el editor y el compilador resuelvan los imports.
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
}
}
Pídele a Claude Code que inspeccione antes de tocar archivos.
Quiero añadir shadcn/ui a este proyecto Vite + React + TypeScript.
Primero lee package.json, vite.config.ts, tsconfig*.json y src/index.css.
Indica solo la configuración que falta. Cuando apruebe el plan,
ejecuta shadcn@latest init y añade los componentes necesarios.
Después, ejecuta los comandos:
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
La guía actual de formularios de shadcn/ui usa React Hook Form, Zod y el componente Field. Evita copiar fragmentos antiguos que asumen otra estructura.
Button y Card: empieza pequeño
El primer componente debe ser pequeño y revisable. Mantén los componentes generados por shadcn/ui en src/components/ui y coloca los componentes específicos de producto en 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>
)
}
Un prompt de revisión útil sería:
Revisa solo src/components/app/ProjectSummaryCard.tsx.
Comprueba tipos de props, imports de shadcn/ui, legibilidad de Tailwind y accesibilidad.
Sugiere cambios en otros archivos solo si son necesarios y explica el motivo.
Form con Field, React Hook Form y Zod
Un formulario real necesita más que inputs bonitos. Debe validar, mostrar errores, usar aria-invalid, deshabilitar el envío mientras se procesa y preparar el camino para errores del 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>
)
}
En formularios pequeños, esquema y componente pueden vivir juntos. Si crecen, pide a Claude Code una separación explícita entre schema.ts, ContactForm.tsx y acciones de servidor.
Dialog y Table sin romper accesibilidad
Dialog no es solo una caja flotante. Radix UI gestiona modo modal, foco, cierre con Escape y anuncios para lectores de pantalla mediante Title y Description. shadcn/ui aporta estilo, pero esas piezas deben conservarse.
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>
</>
)
}
En una app real, deja la carga de datos en la página y pasa customers y onEdit al componente. Así Claude Code no cambia la capa de datos cuando solo quieres ajustar UI.
Tokens de diseño y configuración Tailwind
Los tokens de diseño son nombres para valores visuales: color, radio, sombra, tipografía, espaciado. En Tailwind v4, @theme influye en las utilidades disponibles. shadcn/ui, por su parte, usa variables CSS como --background, --primary y --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);
}
Si el proyecto sigue en Tailwind v3, pide dos planes separados: migrar a v4 o hacer un parche mínimo compatible con v3. Mezclar ambos enfoques suele terminar en clases que no aplican y tokens duplicados.
Evita la deriva por copiar y pegar
La libertad de shadcn/ui exige reglas. Sin ellas, el equipo termina con variantes duplicadas y componentes casi iguales.
| Regla | Motivo |
|---|---|
src/components/ui queda para primitivas de shadcn/ui | Facilita comparar cambios |
src/components/app guarda UI del producto | Separa lógica de negocio |
No cambiar alias de components.json sin motivo | Evita ruido en imports |
| Dar archivos objetivo a Claude Code | Reduce duplicados |
Los archivos de src/components/ui vienen de shadcn/ui.
No crees un nuevo Button ni un nuevo Card.
Usa imports existentes y coloca UI específica en src/components/app.
Después ejecuta rg "function .*Button|export .*Button" src/components
e informa si aparecieron duplicados.
git diff -- src/components/ui
git diff -- src/components/app
pnpm lint
pnpm build
Para mejorar el flujo diario, consulta también los tips de productividad con Claude Code.
Comprobación visual con Playwright
Playwright crea una captura base y compara ejecuciones posteriores. El sistema operativo, navegador, fuentes y viewport deben ser lo más parecidos posible al entorno 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,
})
})
Actualiza snapshots solo cuando el cambio visual sea intencional.
pnpm playwright test --update-snapshots
Cuando falle, dale a Claude Code un problema concreto.
La captura customers-dialog.png falló.
En móvil, los botones del footer del Dialog están demasiado juntos.
Lee solo src/components/app/CustomerTable.tsx y CSS relacionado.
Corrige el espaciado sin eliminar DialogTitle ni DialogDescription.
Tres casos de uso
Primero, un panel de administración nuevo. Button, Card, Table y Dialog cubren listas, detalle, edición y confirmación. Pide primero UI con datos estáticos y luego conexión a API.
Segundo, estandarizar un producto existente. Cambia una búsqueda, un Dialog de ajustes o una Card de estado vacío antes de intentar un rediseño completo.
Tercero, prototipos de pago o demos para clientes. Puedes permitir datos mock, pero exige límites de componentes cercanos a producción para no tirar el trabajo después.
La accesibilidad también encaja bien aquí. Combina este flujo con la guía de accesibilidad con Claude Code para revisar labels, teclado, estructura de Dialog y mensajes de error.
Errores frecuentes
El error más común es pegar configuración antigua de Tailwind en un proyecto v4. Revisa versión y pipeline antes de cambiar CSS o config.
Otro error es poner lógica de negocio en components/ui. Una variante de Button para planes de pago acaba afectando pantallas no relacionadas; mejor crear un wrapper en la capa app.
En Dialog, no elimines DialogTitle porque estorba visualmente. Si no debe verse, escóndelo de forma intencional manteniendo la información para lectores de pantalla.
En formularios, required no basta para errores traducidos, validación asíncrona y errores de servidor. Define schema y renderizado de errores desde el inicio.
En Playwright, evita generar snapshots en un entorno distinto al de CI. Las fuentes y el viewport también importan.
Prompt listo y resultado probado
Objetivo: añadir shadcn/ui a una pantalla admin Vite + React + TypeScript.
Usar Button, Card, Form basado en Field, Dialog y Table.
Restricciones:
- Seguir documentación oficial actual
- Mantener src/components/ui para primitivas shadcn/ui
- Colocar componentes de app en src/components/app
- Usar React Hook Form + Zod + Field
- Conservar DialogTitle y DialogDescription
- Explicar cambios de Tailwind v4 @theme y variables CSS
- Añadir una prueba Playwright toHaveScreenshot
Proceso:
1. Inspeccionar configuración existente
2. Proponer plan
3. Implementar tras aprobación
4. Ejecutar pnpm build y pnpm playwright test
5. Resumir archivos cambiados y riesgos
Resultado probado: en una app Vite mínima pude reproducir la instalación de componentes, añadir un color de marca con @theme, crear un formulario con Field, abrir un Dialog desde una fila de Table y ejecutar una comprobación visual con Playwright. La única segunda pasada fue el renderizado de errores del formulario; al pedir explícitamente aria-invalid y FieldError, el diff quedó claro.
Si tu equipo quiere repetir este patrón, guarda el prompt en CLAUDE.md. ClaudeCodeLab también ofrece plantillas de pago con reglas de UI, checklists de revisión y prompts de Claude Code para equipos que quieren reducir explicaciones repetidas en cada sprint.
PDF gratis: cheatsheet de Claude Code
Introduce tu email y descarga una hoja con comandos, hábitos de revisión y flujos seguros.
Cuidamos tus datos y no enviamos spam.
Sobre el autor
Masa
Ingeniero enfocado en workflows prácticos con Claude Code.
Artículos relacionados
Workflow de Obsidian a CLAUDE.md con Claude Code
Convierte notas de trabajo de Obsidian en notas operativas de CLAUDE.md para no repetir contexto.
Claude Code Revenue CTA Routing: de artículos a PDF, Gumroad y consulta
Un flujo con Claude Code para dirigir lectores a PDF gratis, Gumroad o consulta según intención.
Reglas de handoff para equipos con Claude Code: evidencia, permisos, rollback e ingresos
Formato práctico para entregar trabajo de Claude Code con pruebas, permisos, rollback, PDF gratis, Gumroad y consulta.