AGENTS.md
Compiled from rules for shadcn-best-practices
title: Component Imports impact: HIGH impactDescription: Prevents import path errors
Component Import Patterns
Correct import paths for shadcn/ui components.
title: cn() Utility impact: HIGH impactDescription: Prevents className conflicts
The cn() Utility
Using the className merging utility.
title: Form Building impact: HIGH impactDescription: Type-safe form validation
Form Building with Zod & React Hook Form
Building forms with proper validation.
title: Theming System impact: MEDIUM impactDescription: Consistent styling across themes
Theming System
CSS variables and dark mode configuration.
title: Data Tables impact: MEDIUM impactDescription: Reusable table patterns
Data Tables with TanStack Table
Building accessible, sortable tables.
title: Configuration impact: LOW impactDescription: Project setup accuracy
Components Configuration
Understanding components.json and aliases.
title: The cn() Utility impact: HIGH impactDescription: Prevents Tailwind class conflicts tags: className, tailwind, cn, utility
The cn() Utility
The cn() utility combines clsx and tailwind-merge for handling Tailwind className conflicts.
Incorrect:
// Wrong - template literals don't handle conflicts
className={`base-class ${variant} ${className}`}
// Wrong - clsx alone misses tailwind-merge
import { clsx } from "clsx"
className={clsx("base", variant, className)}
Correct:
// Using cn() handles conflicts properly
import { cn } from "@/lib/utils"
className={cn(
"base-class",
variant === "default" && "bg-slate-900",
className
)}
className={cn("rounded-md", isError && "bg-red-500")}
Reference: shadcn Combining Classes
title: Component Import Paths impact: HIGH impactDescription: Using correct paths prevents build errors tags: imports, paths, components
Component Import Paths
Always use the alias path configured in components.json (default: @/components/ui/).
Incorrect:
// Wrong - relative paths
import { Button } from "../../components/ui/button"
import { Button } from "./components/ui/button"
// Wrong - bypassing shadcn
import { Button } from "@radix-ui/react-slot"
Correct:
// Use the alias path
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Card, CardHeader, CardContent } from "@/components/ui/card"
// Import utilities from configured path
import { cn } from "@/lib/utils"
Reference: shadcn Installation
title: components.json Configuration impact: HIGH impactDescription: Central configuration for shadcn setup tags: config, components-json, radix, base-ui, primitive
components.json Configuration
The components.json file is the central configuration for shadcn/ui projects. It defines which primitive library, styling system, and options your project uses.
Style Options
The style property determines which primitive library and visual preset to use:
| Style | Primitive Library | Description |
|---|---|---|
default | Radix UI | Classic shadcn/ui style |
new-york | Radix UI | Modern style with unified radix-ui package |
radix-vega | Radix UI | Classic look — clean, neutral, familiar |
radix-nova | Radix UI | Compact layouts with reduced padding/margins |
radix-maia | Radix UI | Soft and rounded with generous spacing |
radix-lyra | Radix UI | Boxy and sharp, pairs well with mono fonts |
radix-mira | Radix UI | Compact, made for dense interfaces |
base-vega | Base UI | Classic shadcn/ui look |
base-nova | Base UI | Compact layouts |
base-maia | Base UI | Soft and rounded |
base-lyra | Base UI | Boxy and sharp |
base-mira | Base UI | Dense interfaces |
Radix UI vs Base UI
Choose Base UI when:
- Building a new project and want the modern recommended approach
- Bundle size is critical (Base UI is more lightweight)
- You need modern features like multi-select, combobox with built-in autocomplete
- You prefer the
renderprop pattern overasChild - You're starting fresh and want the latest tooling
Choose Radix UI when:
- Maintaining an existing Radix-based project
- Need specific Radix features or encountered Base UI bugs
- Want wider community resources and examples
- Migrating from an older shadcn setup
| Aspect | Radix UI | Base UI |
|---|---|---|
| Package | @radix-ui/react-* or radix-ui | @base-ui/react |
| Maintainer | WorkOS | MUI team |
| API Pattern | asChild prop | render prop |
| Bundle Size | Larger | Smaller |
| Component Coverage | Mature | Growing (v1.0 stable) |
Full Configuration Example
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "tailwind.config.js",
"css": "app/globals.css",
"baseColor": "slate",
"cssVariables": true
},
"iconLibrary": "lucide",
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"menuColor": "default",
"menuAccent": "subtle",
"rtl": false
}
New Projects
For new projects, use npx shadcn create which provides an interactive setup:
npx shadcn create
This lets you choose:
- Framework (Next.js, Vite, TanStack Start, etc.)
- Primitive library (Radix or Base UI)
- Style preset (vega, nova, maia, lyra, mira)
- Base color, icons, and other options
Migration Commands
# Migrate to unified radix-ui package (for new-york style)
npx shadcn@latest migrate radix
# Migrate existing project to RTL support
npx shadcn@latest migrate rtl
# Migrate to Base UI (if choosing Base UI for new project)
# Use shadcn create and select Base UI - handles setup automatically
npx shadcn create
Identifying Project Library
To identify which primitive library a project uses:
-
Check components.json — Look at the
stylefieldbase-*styles use Base UI- Other styles use Radix UI
-
Check package.json — Look at dependencies
- Base UI:
@base-ui/react - Radix:
@radix-ui/react-*orradix-ui
- Base UI:
-
Check component imports — The installed components differ
- Base UI components use Base UI primitives
- Radix components use Radix primitives
Key Differences in Practice
The API remains the same regardless of choice:
// Works identically whether using Radix or Base UI
import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog"
import { Select, SelectContent, SelectItem } from "@/components/ui/select"
The difference is in the underlying implementation in components/ui/.
Reference: components.json Schema Reference: shadcn Changelog
title: Data Tables with TanStack Table impact: MEDIUM impactDescription: Reusable, accessible table patterns tags: tables, tanstack, sorting, pagination, filtering
Data Tables with TanStack Table
Use TanStack Table (v8) for table logic with shadcn Table components.
Incorrect:
// Building table from scratch
function BadTable({ data }) {
return (
<table>
{data.map((item) => (
<tr key={item.id}>
<td>{item.name}</td>
</tr>
))}
</table>
)
}
Correct:
import {
ColumnDef,
useReactTable,
getCoreRowModel,
getPaginationRowModel,
getSortedRowModel,
} from "@tanstack/react-table"
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table"
interface User {
id: string
name: string
email: string
}
const columns: ColumnDef<User>[] = [
{ accessorKey: "name", header: "Name" },
{ accessorKey: "email", header: "Email" },
]
export function DataTable({ data }: { data: User[] }) {
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
})
return (
<div className="rounded-md border">
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<TableHead key={header.id}>
{header.isPlaceholder
? null
: header.column.columnDef.header}
</TableHead>
))}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow key={row.id}>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{cell.getContext().getValue() as string}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={columns.length} className="h-24 text-center">
No results.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
)
}
Reference: TanStack Table
title: Form Building with Zod & React Hook Form impact: HIGH impactDescription: Type-safe, accessible forms tags: forms, zod, react-hook-form, validation
Form Building with Zod & React Hook Form
Use Zod for schema validation and React Hook Form for form state management.
Incorrect:
// Using uncontrolled components
function BadForm() {
return (
<form>
<input name="email" />
<button type="submit">Submit</button>
</form>
)
}
// Missing FormControl wrapper
<FormField
name="email"
render={({ field }) => (
<FormItem>
<Input {...field} />
</FormItem>
)}
/>
Correct:
import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form"
import { z } from "zod"
const formSchema = z.object({
email: z.string().email("Invalid email address"),
password: z.string().min(8, "Password must be at least 8 characters"),
})
export function LoginForm() {
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: { email: "", password: "" },
})
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit" disabled={form.formState.isSubmitting}>
Submit
</Button>
</form>
</Form>
)
}
Reference: shadcn Forms
title: Theming System impact: MEDIUM impactDescription: Consistent styling across light/dark themes tags: theming, dark-mode, css-variables, next-themes
Theming System
Use CSS variables defined in globals.css for theming, and next-themes for dark mode.
Incorrect:
// Hardcoded colors
<Button className="bg-blue-500 text-white" />
// Manual dark mode
<div className={isDark ? "dark" : ""}>
Correct:
// Using CSS variables
<Button className="bg-primary text-primary-foreground" />
// ThemeProvider setup
"use client"
import { ThemeProvider } from "@/components/theme-provider"
export default function Layout({ children }) {
return (
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
>
{children}
</ThemeProvider>
)
}
// Theme toggle
import { useTheme } from "next-themes"
function ThemeToggle() {
const { setTheme, theme } = useTheme()
return (
<button onClick={() => setTheme(theme === "light" ? "dark" : "light")}>
Toggle
</button>
)
}
Reference: shadcn Theming