shadcn/ui dengan Claude Code: panduan praktis React
Gunakan Claude Code dengan shadcn/ui: setup, Button/Card/Form/Dialog/Table, design token, dan cek Playwright.
shadcn/ui bukan library komponen biasa yang hanya di-import dari node_modules. CLI-nya menyalin source code komponen ke project Anda, biasanya ke src/components/ui. Artinya Button, Card, Dialog, dan Table menjadi bagian dari codebase sendiri. Anda bebas mengubahnya, tetapi tim juga harus menjaga konsistensinya.
Model seperti ini cocok dengan Claude Code. Claude Code bisa membaca file yang dihasilkan, memahami class Tailwind, mengubah variant, menyambungkan form, dan menambahkan test Playwright. Namun prompt yang terlalu luas seperti “buatkan dashboard” bisa menghasilkan Button duplikat, konfigurasi Tailwind lama, atau Dialog yang kehilangan struktur aksesibilitas.
Panduan ini memakai contoh Vite + React + TypeScript: install shadcn/ui, tambah Button/Card/Form/Dialog/Table, rapikan design token, cegah copy-paste drift, dan validasi tampilan dengan Playwright. Jika Anda baru mulai, baca dulu panduan awal Claude Code.
Mulai dari dokumentasi resmi
Sebelum mengubah file, beri Claude Code sumber resmi yang harus diikuti. Dokumentasi Vite shadcn/ui menggunakan shadcn@latest, Tailwind CSS v4 menjelaskan theme variables melalui @theme, Radix UI mendokumentasikan perilaku Dialog yang accessible, dan Playwright memakai toHaveScreenshot() untuk visual comparison.
- 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 membaca project"] --> B["shadcn/ui init"]
B --> C["Tambah komponen dasar"]
C --> D["Pusatkan design token"]
D --> E["Buat komponen app"]
E --> F["Cek dengan Playwright"]
Instalasi dan inisialisasi
Contoh ini memakai Vite agar fokus tetap kecil. Prinsipnya bisa dipakai di Next.js, tetapi pemula sebaiknya tidak langsung mencampur UI system, routing, dan Server Components.
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
Atur Tailwind plugin dan alias @ di 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"),
},
},
})
Tambahkan alias yang sama di TypeScript.
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
}
}
Minta Claude Code melakukan inspeksi lebih dulu.
Saya ingin menambahkan shadcn/ui ke project Vite + React + TypeScript ini.
Baca dulu package.json, vite.config.ts, tsconfig*.json, dan src/index.css.
Laporkan hanya setup yang kurang. Setelah saya setujui,
jalankan shadcn@latest init dan tambah komponen yang diperlukan.
Lalu jalankan command berikut.
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
Dokumentasi form shadcn/ui saat ini menggunakan React Hook Form, Zod, dan komponen Field. Jangan langsung menyalin snippet lama tanpa memastikan cocok dengan versi project.
Mulai dari Button dan Card
Jangan langsung membangun dashboard penuh. Buat komponen kecil yang mudah direview. File hasil shadcn/ui tetap di src/components/ui, sedangkan UI khusus produk masuk ke 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>
)
}
Gunakan prompt review yang sempit.
Review hanya src/components/app/ProjectSummaryCard.tsx.
Periksa tipe props, import shadcn/ui, keterbacaan class Tailwind, dan aksesibilitas.
Sarankan perubahan file lain hanya jika wajib, lengkap dengan alasannya.
Form dengan Field, React Hook Form, dan Zod
Form production tidak cukup hanya menampilkan input. Anda perlu validasi, pesan error, aria-invalid, tombol submit disabled saat proses, autocomplete, dan ruang untuk error dari server.
"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>
)
}
Untuk form kecil, schema dan komponen boleh berada di satu file. Jika field bertambah, minta Claude Code memisahkan schema.ts, ContactForm.tsx, dan server action secara sengaja.
Dialog dan Table yang aman
Dialog bukan sekadar popup. Radix UI menangani mode modal, focus trap, tombol Escape, dan pengumuman screen reader melalui Title dan Description. shadcn/ui memberi styling, tetapi struktur itu harus tetap ada.
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>
</>
)
}
Di aplikasi nyata, biarkan page layer mengambil data lalu kirim customers dan onEdit ke komponen. Dengan begitu Claude Code tidak mengubah data layer saat tugasnya hanya memperbaiki UI.
Design token dan Tailwind
Design token adalah nilai bernama untuk warna, radius, shadow, font, dan spacing. Di Tailwind v4, @theme memengaruhi utility yang dibuat. Komponen shadcn/ui juga memakai CSS variable seperti --background, --primary, dan --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);
}
Jika project masih memakai Tailwind v3, minta dua rencana terpisah: migrasi ke v4 atau patch minimal yang tetap kompatibel dengan v3. Mencampur keduanya membuat bug styling sulit dilacak.
Cegah copy-paste drift
shadcn/ui memberi source code. Tanpa aturan, tim mudah membuat Button dan Card yang hampir sama di banyak tempat.
| Aturan | Alasan |
|---|---|
src/components/ui khusus primitive shadcn/ui | Perbedaan mudah dilacak |
UI produk masuk src/components/app | Logika bisnis tidak bocor ke base layer |
Jangan mengubah alias components.json tanpa alasan | Import tetap stabil |
| Beri Claude Code file target | Duplikasi berkurang |
File di src/components/ui berasal dari shadcn/ui.
Jangan membuat Button atau Card baru.
Gunakan import yang ada dan letakkan UI produk di src/components/app.
Setelah edit, jalankan rg "function .*Button|export .*Button" src/components
dan laporkan apakah ada duplikasi.
git diff -- src/components/ui
git diff -- src/components/app
pnpm lint
pnpm build
Untuk workflow yang lebih rapi, lihat juga tips produktivitas Claude Code.
Visual check dengan Playwright
Playwright membuat screenshot baseline lalu membandingkan run berikutnya. Sistem operasi, browser, font, dan viewport sebaiknya sama dengan 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,
})
})
Update snapshot hanya jika perubahan visual memang disengaja.
pnpm playwright test --update-snapshots
Saat gagal, beri Claude Code masalah yang spesifik.
Playwright customers-dialog.png gagal.
Di mobile, tombol footer Dialog terlalu rapat.
Baca hanya src/components/app/CustomerTable.tsx dan CSS terkait.
Perbaiki spacing tanpa menghapus DialogTitle atau DialogDescription.
Tiga use case praktis
Pertama, dashboard admin baru. Button, Card, Table, dan Dialog sudah cukup untuk list, detail, edit, dan confirmation flow. Buat UI statis dulu, baru sambungkan API.
Kedua, standardisasi produk lama. Ganti search form, settings Dialog, atau empty-state Card satu per satu sebelum mencoba redesign besar.
Ketiga, prototype berbayar atau demo klien. Data mock boleh, tetapi batas komponen harus dekat dengan production agar kode bisa dilanjutkan.
Untuk aksesibilitas, gabungkan workflow ini dengan praktik aksesibilitas Claude Code. Periksa label, keyboard behavior, struktur Dialog, dan pesan error.
Kesalahan umum
Kesalahan pertama adalah menempel konfigurasi Tailwind lama ke project v4. Cek versi dan pipeline build sebelum edit.
Kesalahan kedua adalah memasukkan logika bisnis ke components/ui. Variant Button khusus paket berbayar sebaiknya menjadi wrapper di layer app.
Kesalahan ketiga adalah menghapus DialogTitle karena tidak cocok secara visual. Jika tidak ingin terlihat, sembunyikan dengan sengaja tanpa menghapus informasinya.
Kesalahan keempat adalah hanya mengandalkan required. Form production butuh error terjemahan, validasi async, dan error server.
Kesalahan kelima adalah membuat baseline Playwright di environment berbeda dari CI. Font dan viewport saja bisa memicu diff.
Prompt siap pakai dan hasil uji
Goal: tambahkan shadcn/ui ke layar admin Vite + React + TypeScript.
Gunakan Button, Card, Form berbasis Field, Dialog, dan Table.
Constraints:
- Ikuti dokumentasi resmi saat ini
- src/components/ui hanya untuk primitive shadcn/ui
- Komponen khusus app masuk src/components/app
- Form memakai React Hook Form + Zod + Field
- Pertahankan DialogTitle dan DialogDescription
- Jelaskan perubahan Tailwind v4 @theme dan CSS variables
- Tambahkan satu test Playwright toHaveScreenshot
Process:
1. Inspect setup yang ada
2. Usulkan rencana perubahan
3. Implementasi setelah approval
4. Jalankan pnpm build dan pnpm playwright test
5. Ringkas file berubah dan risiko
Hasil uji: di app Vite minimal, workflow ini berhasil mereproduksi instalasi komponen, penambahan warna brand dengan @theme, form berbasis Field, Dialog yang dibuka dari baris Table, dan visual check Playwright. Bagian yang perlu satu putaran lagi hanya render error form; setelah meminta aria-invalid dan FieldError secara eksplisit, diff menjadi jelas.
Untuk tim, simpan prompt ini di CLAUDE.md. ClaudeCodeLab juga menyediakan template berbayar berisi aturan UI, checklist review, dan prompt Claude Code agar tim tidak mengulang penjelasan yang sama di setiap sprint.
PDF gratis: cheatsheet Claude Code
Masukkan email dan unduh satu halaman berisi command, kebiasaan review, dan workflow aman.
Kami menjaga datamu dan tidak mengirim spam.
Tentang penulis
Masa
Engineer yang berfokus pada workflow Claude Code praktis dan adopsi tim.
Artikel terkait
Workflow Obsidian ke CLAUDE.md untuk Claude Code
Ubah catatan kerja Obsidian menjadi operating note CLAUDE.md agar konteks tidak dijelaskan ulang.
Claude Code Revenue CTA Routing: dari artikel ke PDF, Gumroad, dan konsultasi
Workflow Claude Code untuk mengarahkan pembaca ke PDF gratis, Gumroad, atau konsultasi sesuai intent.
Aturan handoff tim Claude Code: bukti review, permission, rollback, dan jalur revenue
Format handoff Claude Code untuk tim: bukti, permission rule, rollback, PDF gratis, Gumroad, dan konsultasi.