Use Cases (Actualizado: 2/6/2026)

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 con Claude Code: guía práctica para React

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.

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.

ReglaMotivo
src/components/ui queda para primitivas de shadcn/uiFacilita comparar cambios
src/components/app guarda UI del productoSepara lógica de negocio
No cambiar alias de components.json sin motivoEvita ruido en imports
Dar archivos objetivo a Claude CodeReduce 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.

#Claude Code #shadcn/ui #React #Tailwind CSS #Biblioteca UI
Gratis

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.

Masa

Sobre el autor

Masa

Ingeniero enfocado en workflows prácticos con Claude Code.