shadcn/ui Skills
Status: Next.js 16 ready
Package Manager: pnpm (required)
Official Docs:
Triggers
- shadcn
- shadcn/ui
- radix
- ui components
- component library
- tailwind components
- button component
- dialog component
- form component
- data table
Table of Contents
- Installation & Setup
- Project Configuration (Next.js 16)
- Components & Usage
- Blocks
- Forms
- Sonner (Toast Replacement)
- Dark Mode
- MCP Integration
- Best Practices
Installation & Setup
- Use pnpm for all commands.
- Scaffold shadcn/ui in an existing Next.js 16 app:
pnpm dlx shadcn@latest init
- Accept prompts for Next.js + TypeScript.
- CLI creates
components.json and installs required deps (Tailwind, class utilities).
- Add components when needed (keeps bundle small):
pnpm dlx shadcn@latest add button card input textarea select
# add blocks or utilities on demand
- After adding components, run:
pnpm lint && pnpm test # if configured
pnpm dev # verify styles render
Project Configuration (Next.js 16)
- Tailwind is required. Ensure
tailwind.config.(ts|js) includes shadcn paths:// tailwind.config.ts
import type { Config } from "tailwindcss"
import { fontFamily } from "tailwindcss/defaultTheme"
const config: Config = {
darkMode: ["class"],
content: [
"./app/**/*.{ts,tsx}",
"./components/**/*.{ts,tsx}",
"./src/**/*.{ts,tsx}",
],
theme: {
extend: {
fontFamily: {
sans: ["var(--font-sans)", ...fontFamily.sans],
},
},
},
plugins: [require("tailwindcss-animate")],
}
export default config
- Use App Router and Server Components by default; mark client files with
"use client".
- Keep
globals.css with CSS variables from shadcn init; do not remove the color tokens.
Components & Usage
Blocks
Forms
- Use the provided Form primitives with
react-hook-form + @hookform/resolvers/zod:"use client"
import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form"
import { z } from "zod"
import { Button } from "@/components/ui/button"
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"
import { Input } from "@/components/ui/input"
const schema = z.object({
email: z.string().email(),
name: z.string().min(2),
})
export function ProfileForm() {
const form = useForm<z.infer<typeof schema>>({
resolver: zodResolver(schema),
defaultValues: { email: "", name: "" },
})
return (
<Form {...form}>
<form className="space-y-4">
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input placeholder="you@example.com" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit">Save</Button>
</form>
</Form>
)
}
- Keep validation schemas colocated; surface errors via
FormMessage.
Sonner (Toast Replacement)
import { Toaster } from "@/components/ui/sonner"
import { toast } from "sonner"
export function RootProviders({ children }: { children: React.ReactNode }) {
return (
<>
{children}
<Toaster richColors position="top-right" />
</>
)
}
// usage
toast.success("Profile saved")
- Keep Sonner provider outside
app/(marketing) vs app/(dashboard) duplication to avoid multiple toasters.
Dark Mode
- Use class-based theming with
next-themes.
- Example provider (
components/theme-provider.tsx):"use client"
import { ThemeProvider as NextThemesProvider } from "next-themes"
export function ThemeProvider({ children }: { children: React.ReactNode }) {
return (
<NextThemesProvider attribute="class" defaultTheme="system" enableSystem disableTransitionOnChange>
{children}
</NextThemesProvider>
)
}
- Wrap the App Router layout:
// app/layout.tsx
import { ThemeProvider } from "@/components/theme-provider"
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" suppressHydrationWarning>
<body>
<ThemeProvider>{children}</ThemeProvider>
</body>
</html>
)
}
- Keep
suppressHydrationWarning on <html> to avoid mismatches when switching themes.
MCP Integration
- shadcn/ui ships an MCP server (see docs) so agents can browse/add components and blocks safely.
- Register the server in your MCP client config, pointing at your project root where
components.json lives.
- Prefer MCP-driven adds over manual copy/paste to keep component versions consistent with the catalog.
Component Architecture
shadcn/ui is not a library — components are copied into your project. You own them.
File Structure
src/
├── components/
│ ├── ui/ # shadcn components (don't modify directly)
│ │ ├── button.tsx
│ │ ├── card.tsx
│ │ └── dialog.tsx
│ ├── blocks/ # higher-level page sections
│ └── [custom]/ # your composed components
│ └── loading-button.tsx
├── lib/
│ └── utils.ts # cn() utility
└── app/
└── page.tsx
The cn() Utility
All shadcn components use this for class merging:
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
Component Variants with cva
Use class-variance-authority for variant logic:
import { cva } from "class-variance-authority"
const buttonVariants = cva(
"inline-flex items-center justify-center rounded-md",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground",
outline: "border border-input",
destructive: "bg-destructive text-destructive-foreground",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
},
},
defaultVariants: { variant: "default", size: "default" },
}
)
Extending Components
Create wrappers in components/ (not components/ui/):
// components/loading-button.tsx
import { Button, type ButtonProps } from "@/components/ui/button"
import { Loader2 } from "lucide-react"
export function LoadingButton({
loading, children, ...props
}: ButtonProps & { loading?: boolean }) {
return (
<Button disabled={loading} {...props}>
{loading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
{children}
</Button>
)
}
Visual Styles
shadcn/ui supports multiple visual styles:
- default — clean, minimal
- new-york — classic, refined
- Vega, Nova, Maia, Lyra, Mira — newer visual themes
Set during init or in components.json.
Block Categories
Blocks from ui.shadcn.com/blocks by category:
- calendar — Calendar interfaces
- dashboard — Dashboard layouts and analytics
- login — Authentication flows (sign-in, sign-up, forgot password)
- sidebar — Navigation sidebars with collapsible sections
- products — E-commerce cards, grids, detail pages
Best Practices
- Stay modular: Add only the components/blocks you need; trim unused files to keep bundles small.
- Respect tokens: Do not hardcode colors; use the CSS variables set up by init.
- Accessibility: Keep aria labels, roles, and keyboard interactions from the upstream examples.
- Typography: Use CSS variables +
next/font to load fonts; wire them into --font-sans in globals.css.
- Extend, don't modify: Create wrapper components in
components/ — leave components/ui/ pristine for easy updates.
- LLM usage: Follow llms.txt when generating code; prefer pnpm commands and Sonner over legacy toast.
Verify
- The change was rendered in a browser/simulator and a screenshot or DOM snapshot was captured, not just code-reviewed
- Layout was checked at the breakpoints the shadcn guide calls out (mobile + desktop minimum); evidence of each is attached
- Color, typography, and spacing values used come from the project's design tokens / theme, not hard-coded ad-hoc values
- Keyboard navigation and focus order were exercised on every interactive element introduced
- Reduced-motion / dark-mode (when supported) variants were verified, not assumed to inherit
- No console errors or hydration warnings were emitted during the verification render
1---2name: shadcn3description: Installation, components, blocks, forms, theming, and MCP guidance for shadcn/ui in modern Next.js projects using pnpm4---56# shadcn/ui Skills78**Status:** Next.js 16 ready 9**Package Manager:** pnpm (required) 10**Official Docs:** 11- [Installation (Next.js)](https://ui.shadcn.com/docs/installation/next) 12- [Components](https://ui.shadcn.com/docs/components) 13- [Blocks](https://ui.shadcn.com/blocks) 14- [Sonner (toast replacement)](https://ui.shadcn.com/docs/components/sonner) 15- [Forms](https://ui.shadcn.com/docs/components/form) 16- [Dark Mode (Next.js)](https://ui.shadcn.com/docs/dark-mode/next) 17- [MCP Server](https://ui.shadcn.com/docs/mcp) 18- [LLM Guidelines](https://ui.shadcn.com/llms.txt)1920---212223## Triggers2425- shadcn26- shadcn/ui27- radix28- ui components29- component library30- tailwind components31- button component32- dialog component33- form component34- data table3536## Table of Contents37381. [Installation & Setup](#installation--setup)392. [Project Configuration (Next.js 16)](#project-configuration-nextjs-16)403. [Components & Usage](#components--usage)414. [Blocks](#blocks)425. [Forms](#forms)436. [Sonner (Toast Replacement)](#sonner-toast-replacement)447. [Dark Mode](#dark-mode)458. [MCP Integration](#mcp-integration)469. [Best Practices](#best-practices)4748---4950## Installation & Setup5152- Use pnpm for all commands.53- Scaffold shadcn/ui in an existing Next.js 16 app:54 ```bash55 pnpm dlx shadcn@latest init56 ```57 - Accept prompts for **Next.js** + **TypeScript**.58 - CLI creates `components.json` and installs required deps (Tailwind, class utilities).59- Add components when needed (keeps bundle small):60 ```bash61 pnpm dlx shadcn@latest add button card input textarea select62 # add blocks or utilities on demand63 ```64- After adding components, run:65 ```bash66 pnpm lint && pnpm test # if configured67 pnpm dev # verify styles render68 ```6970---7172## Project Configuration (Next.js 16)7374- Tailwind is required. Ensure `tailwind.config.(ts|js)` includes shadcn paths:75 ```ts76 // tailwind.config.ts77 import type { Config } from "tailwindcss"78 import { fontFamily } from "tailwindcss/defaultTheme"7980 const config: Config = {81 darkMode: ["class"],82 content: [83 "./app/**/*.{ts,tsx}",84 "./components/**/*.{ts,tsx}",85 "./src/**/*.{ts,tsx}",86 ],87 theme: {88 extend: {89 fontFamily: {90 sans: ["var(--font-sans)", ...fontFamily.sans],91 },92 },93 },94 plugins: [require("tailwindcss-animate")],95 }9697 export default config98 ```99- Use App Router and Server Components by default; mark client files with `"use client"`.100- Keep `globals.css` with CSS variables from shadcn init; do not remove the color tokens.101102---103104## Components & Usage105106- Components live in `components/ui`. Import directly:107 ```tsx108 import { Button } from "@/components/ui/button"109110 export function CTA() {111 return <Button size="lg">Get started</Button>112 }113 ```114- Many components support `asChild` to compose with links:115 ```tsx116 <Button asChild>117 <Link href="/docs">Docs</Link>118 </Button>119 ```120- Keep icons in `components/ui/icons` or use `lucide-react` (installed during init).121- Reference component docs for props and accessibility expectations.122123---124125## Blocks126127- Blocks are higher-level page sections from [ui.shadcn.com/blocks](https://ui.shadcn.com/blocks).128- Add a block via CLI (preferred to avoid copy/paste drift):129 ```bash130 pnpm dlx shadcn@latest add blocks/application-shells/sidebar-02131 ```132- Blocks follow the same theming and Tailwind tokens as core components; adjust spacing/tokens instead of rewriting styles.133- Keep blocks in `components/blocks/*` to avoid mixing with low-level UI primitives.134135---136137## Forms138139- Use the provided Form primitives with `react-hook-form` + `@hookform/resolvers/zod`:140 ```tsx141 "use client"142143 import { zodResolver } from "@hookform/resolvers/zod"144 import { useForm } from "react-hook-form"145 import { z } from "zod"146 import { Button } from "@/components/ui/button"147 import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"148 import { Input } from "@/components/ui/input"149150 const schema = z.object({151 email: z.string().email(),152 name: z.string().min(2),153 })154155 export function ProfileForm() {156 const form = useForm<z.infer<typeof schema>>({157 resolver: zodResolver(schema),158 defaultValues: { email: "", name: "" },159 })160161 return (162 <Form {...form}>163 <form onSubmit={form.handleSubmit(console.log)} className="space-y-4">164 <FormField165 control={form.control}166 name="email"167 render={({ field }) => (168 <FormItem>169 <FormLabel>Email</FormLabel>170 <FormControl>171 <Input placeholder="you@example.com" {...field} />172 </FormControl>173 <FormMessage />174 </FormItem>175 )}176 />177 <Button type="submit">Save</Button>178 </form>179 </Form>180 )181 }182 ```183- Keep validation schemas colocated; surface errors via `FormMessage`.184185---186187## Sonner (Toast Replacement)188189- `toast` is **deprecated**; use Sonner.190- Add the Sonner component via CLI:191 ```bash192 pnpm dlx shadcn@latest add sonner193 ```194- Mount once in your root layout or top-level provider:195```tsx196import { Toaster } from "@/components/ui/sonner"197import { toast } from "sonner"198199 export function RootProviders({ children }: { children: React.ReactNode }) {200 return (201 <>202 {children}203 <Toaster richColors position="top-right" />204 </>205 )206 }207208 // usage209 toast.success("Profile saved")210 ```211- Keep Sonner provider outside `app/(marketing)` vs `app/(dashboard)` duplication to avoid multiple toasters.212213---214215## Dark Mode216217- Use class-based theming with `next-themes`.218- Example provider (`components/theme-provider.tsx`):219 ```tsx220 "use client"221222 import { ThemeProvider as NextThemesProvider } from "next-themes"223224 export function ThemeProvider({ children }: { children: React.ReactNode }) {225 return (226 <NextThemesProvider attribute="class" defaultTheme="system" enableSystem disableTransitionOnChange>227 {children}228 </NextThemesProvider>229 )230 }231 ```232- Wrap the App Router layout:233 ```tsx234 // app/layout.tsx235 import { ThemeProvider } from "@/components/theme-provider"236237 export default function RootLayout({ children }: { children: React.ReactNode }) {238 return (239 <html lang="en" suppressHydrationWarning>240 <body>241 <ThemeProvider>{children}</ThemeProvider>242 </body>243 </html>244 )245 }246 ```247- Keep `suppressHydrationWarning` on `<html>` to avoid mismatches when switching themes.248249---250251## MCP Integration252253- shadcn/ui ships an MCP server (see [docs](https://ui.shadcn.com/docs/mcp)) so agents can browse/add components and blocks safely.254- Register the server in your MCP client config, pointing at your project root where `components.json` lives.255- Prefer MCP-driven adds over manual copy/paste to keep component versions consistent with the catalog.256257---258259## Component Architecture260261shadcn/ui is **not a library** — components are copied into your project. You own them.262263### File Structure264```265src/266├── components/267│ ├── ui/ # shadcn components (don't modify directly)268│ │ ├── button.tsx269│ │ ├── card.tsx270│ │ └── dialog.tsx271│ ├── blocks/ # higher-level page sections272│ └── [custom]/ # your composed components273│ └── loading-button.tsx274├── lib/275│ └── utils.ts # cn() utility276└── app/277 └── page.tsx278```279280### The `cn()` Utility281282All shadcn components use this for class merging:283```typescript284import { clsx, type ClassValue } from "clsx"285import { twMerge } from "tailwind-merge"286287export function cn(...inputs: ClassValue[]) {288 return twMerge(clsx(inputs))289}290```291292### Component Variants with cva293294Use `class-variance-authority` for variant logic:295```typescript296import { cva } from "class-variance-authority"297298const buttonVariants = cva(299 "inline-flex items-center justify-center rounded-md",300 {301 variants: {302 variant: {303 default: "bg-primary text-primary-foreground",304 outline: "border border-input",305 destructive: "bg-destructive text-destructive-foreground",306 },307 size: {308 default: "h-10 px-4 py-2",309 sm: "h-9 rounded-md px-3",310 lg: "h-11 rounded-md px-8",311 },312 },313 defaultVariants: { variant: "default", size: "default" },314 }315)316```317318### Extending Components319320Create wrappers in `components/` (not `components/ui/`):321```typescript322// components/loading-button.tsx323import { Button, type ButtonProps } from "@/components/ui/button"324import { Loader2 } from "lucide-react"325326export function LoadingButton({327 loading, children, ...props328}: ButtonProps & { loading?: boolean }) {329 return (330 <Button disabled={loading} {...props}>331 {loading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}332 {children}333 </Button>334 )335}336```337338## Visual Styles339340shadcn/ui supports multiple visual styles:341- **default** — clean, minimal342- **new-york** — classic, refined343- **Vega, Nova, Maia, Lyra, Mira** — newer visual themes344345Set during init or in `components.json`.346347## Block Categories348349Blocks from [ui.shadcn.com/blocks](https://ui.shadcn.com/blocks) by category:350- **calendar** — Calendar interfaces351- **dashboard** — Dashboard layouts and analytics352- **login** — Authentication flows (sign-in, sign-up, forgot password)353- **sidebar** — Navigation sidebars with collapsible sections354- **products** — E-commerce cards, grids, detail pages355356## Best Practices3573581. **Stay modular:** Add only the components/blocks you need; trim unused files to keep bundles small.3592. **Respect tokens:** Do not hardcode colors; use the CSS variables set up by init.3603. **Accessibility:** Keep aria labels, roles, and keyboard interactions from the upstream examples.3614. **Typography:** Use CSS variables + `next/font` to load fonts; wire them into `--font-sans` in `globals.css`.3625. **Extend, don't modify:** Create wrapper components in `components/` — leave `components/ui/` pristine for easy updates.3636. **LLM usage:** Follow [llms.txt](https://ui.shadcn.com/llms.txt) when generating code; prefer pnpm commands and Sonner over legacy toast.364365## Verify366367- The change was rendered in a browser/simulator and a screenshot or DOM snapshot was captured, not just code-reviewed368- Layout was checked at the breakpoints the shadcn guide calls out (mobile + desktop minimum); evidence of each is attached369- Color, typography, and spacing values used come from the project's design tokens / theme, not hard-coded ad-hoc values370- Keyboard navigation and focus order were exercised on every interactive element introduced371- Reduced-motion / dark-mode (when supported) variants were verified, not assumed to inherit372- No console errors or hydration warnings were emitted during the verification render