- wat
- components.json
- The
cn()Helper - globals.css & Theming
- Dark Mode
- Dependencies Explained
- Blocks vs Components
- Quick Start Workflow
- Forms with TanStack + shadcn/ui
- Claude Code + MCP Tips
- Essential Commands
- Mental Model
wat
shadcn/ui is a set of beautifully-designed, accessible components and a code distribution platform. Works with your favorite frameworks and AI models. Open Source. Open Code.
components.json
Controls how CLI installs components, paths, and styling preferences.
{
"style": "default", // Component style variant
"rsc": true, // React Server Components
"tsx": true, // TypeScript
"tailwind": {
"config": "tailwind.config.js",
"css": "src/styles/globals.css",
"baseColor": "slate", // Your color scheme
"cssVariables": true // Use CSS vars for theming
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils"
}
}
The cn() Helper
// lib/utils.ts
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
Leverages two packages to level up class name management:
clsx: Conditionally joins classNamestailwind-merge: Intelligently merges Tailwind classes (no conflicts)
Example:
cn("px-4 py-2", isActive && "bg-blue-500", "px-6");
// Result: "py-2 bg-blue-500 px-6" (px-6 wins, no duplicates)
globals.css & Theming
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--primary: 222.2 47.4% 11.2%;
/* ... more CSS variables */
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
/* ... dark variants */
}
}
Key points:
- Uses HSL format:
hue saturation lightness - CSS variables for dynamic theming
- Components reference these:
bg-background,text-foreground - Change colors here, everything updates
Dark Mode
Adds/removes .dark class on <html>, cascade updates CSS variables.
Setup:
Create a theme provider:
// components/theme-provider.tsx
import { createContext, useContext, useEffect, useState } from "react";
type Theme = "dark" | "light" | "system";
type ThemeProviderProps = {
children: React.ReactNode;
defaultTheme?: Theme;
storageKey?: string;
};
type ThemeProviderState = {
theme: Theme;
setTheme: (theme: Theme) => void;
};
const initialState: ThemeProviderState = {
theme: "system",
setTheme: () => null,
};
const ThemeProviderContext = createContext<ThemeProviderState>(initialState);
export function ThemeProvider({
children,
defaultTheme = "system",
storageKey = "vite-ui-theme",
...props
}: ThemeProviderProps) {
const [theme, setTheme] = useState<Theme>(
() => (localStorage.getItem(storageKey) as Theme) || defaultTheme
);
useEffect(() => {
const root = window.document.documentElement;
root.classList.remove("light", "dark");
if (theme === "system") {
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
.matches
? "dark"
: "light";
root.classList.add(systemTheme);
return;
}
root.classList.add(theme);
}, [theme]);
const value = {
theme,
setTheme: (theme: Theme) => {
localStorage.setItem(storageKey, theme);
setTheme(theme);
},
};
return (
<ThemeProviderContext.Provider {...props} value={value}>
{children}
</ThemeProviderContext.Provider>
);
}
export const useTheme = () => {
const context = useContext(ThemeProviderContext);
if (context === undefined)
throw new Error("useTheme must be used within a ThemeProvider");
return context;
};
Wrap root layout:
// App.tsx
import { ThemeProvider } from "@/components/theme-provider";
function App() {
return (
<ThemeProvider defaultTheme="dark" storageKey="vite-ui-theme">
{children}
</ThemeProvider>
);
}
export default App;
Mode toggle:
import { Moon, Sun } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { useTheme } from "@/components/theme-provider";
export function ModeToggle() {
const { setTheme } = useTheme();
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="icon">
<Sun className="h-[1.2rem] w-[1.2rem] scale-100 rotate-0 transition-all dark:scale-0 dark:-rotate-90" />
<Moon className="absolute h-[1.2rem] w-[1.2rem] scale-0 rotate-90 transition-all dark:scale-100 dark:rotate-0" />
<span className="sr-only">Toggle theme</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => setTheme("light")}>
Light
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme("dark")}>
Dark
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme("system")}>
System
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}
Dependencies Explained
| Package | Purpose |
|---|---|
class-variance-authority (cva) | Create component variants with type-safety |
clsx | Conditional className joining |
tailwind-merge | Merge Tailwind classes without conflicts |
lucide-react | Icon library (consistent, tree-shakeable) |
tailwindcss-animate | Pre-built animations for components |
CVA Example
const buttonVariants = cva(
"rounded font-medium", // base
{
variants: {
variant: {
default: "bg-primary text-white",
outline: "border border-input",
},
size: {
sm: "px-3 py-1",
lg: "px-6 py-3",
},
},
}
);
// Usage: buttonVariants({ variant: "outline", size: "lg" })
Blocks vs Components
Components: Individual UI pieces (Button, Input, Card)
npx shadcn@latest add button
Blocks: Pre-built page sections (Login forms, dashboards, sidebars)
npx shadcn@latest add sidebar-01
Key difference: Blocks are opinionated compositions of components for rapid prototyping
Quick Start Workflow
1. Install components as needed:
npx shadcn@latest add button input card form
2. Use in your code:
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
<Button variant="outline">Click me</Button>;
3. Customize directly in component file:
- Components live in
components/ui/ - Edit them freely—they’re YOUR code now
Forms with TanStack + shadcn/ui
Install form components:
npx shadcn@latest add form input label
Basic pattern:
import { useForm } from "@tanstack/react-form";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
export function MyForm() {
const form = useForm({
defaultValues: { email: "" },
onSubmit: async ({ value }) => {
console.log(value);
},
});
return (
<form
onSubmit={(e) => {
e.preventDefault();
form.handleSubmit();
}}
>
<form.Field name="email">
{(field) => (
<Input
value={field.state.value}
onChange={(e) => field.handleChange(e.target.value)}
/>
)}
</form.Field>
<Button type="submit">Submit</Button>
</form>
);
}
Claude Code + MCP Tips
Ask Claude to:
- “Add a login form using shadcn form components”
- “Create a dashboard layout with sidebar-01 block”
- “Build a data table with sorting using shadcn table”
MCP gives Claude:
- Component APIs and props
- Best practices for composition
- Accessibility patterns
Pro tip: Say “use shadcn components” explicitly in prompts
Essential Commands
# Add specific component
npx shadcn@latest add button
# Add multiple
npx shadcn@latest add button input card
# Add a block
npx shadcn@latest add dashboard-01
# Update components
npx shadcn@latest diff
Mental Model
- Install components into your repo (they’re yours to modify)
- Import from
@/components/ui/ - Customize via props or edit source directly
- Theme globally via CSS variables in globals.css
- Extend using CVA for new variants
Not a library you import - it’s a code generator that gives you ownership.