From skills
Setup a design system using shadcn/ui. Use this skill when the user says "setup design system", "set up design system", "create design system", "setup shadcn", or "initialize design system".
How this skill is triggered — by the user, by Claude, or both
Slash command
/skills:add-shadcnThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
The `shadcn/ui` initialization supports custom themes via a URL parameter.
The shadcn/ui initialization supports custom themes via a URL parameter.
Compatibility: This skill works with both standard Next.js projects and projects using --no-src-dir. The component paths (components/) automatically adapt to your project structure.
Use this command
bunx shadcn@latest create $(jq -r .name package.json) --preset "https://ui.shadcn.com/init?base=base&baseColor=gray&theme=emerald&iconLibrary=phosphor&radius=none&style=maia&font=nunito-sans&menuAccent=subtle&menuColor=default&template=next" --template next
After initialization, auto-fix formatting and lint issues in generated components:
bunx biome format --write . && bunx biome check --fix .
Note: The format step fixes whitespace/style, and the check --fix step fixes import sorting (organizeImports) in shadcn-generated files. Both are required — shadcn components ship with unsorted imports that fail biome check.
The shadcn preset includes a demo component-example.tsx file. Remove it:
rm -f src/components/component-example.tsx components/component-example.tsx
After initialization, add the following to app/globals.css (or src/app/globals.css) inside the @layer base block, or as a top-level rule:
html {
@apply h-dvh overscroll-none;
}
This ensures the app fills the dynamic viewport height on mobile and prevents overscroll bounce effects.
After initialization, always post-process the .dark theme block in globals.css to remove chroma from all neutral gray variables. shadcn themes sometimes generate dark grays with blue or purple tint (non-zero chroma in oklch). Mixing these tinted grays with components that use pure neutrals (e.g., React Flow, canvas elements, third-party widgets) creates an ugly warm/cold mismatch.
Rule: Every oklch color in the .dark {} block that represents a neutral gray (background, card, popover, muted, accent, secondary, sidebar, ring, foreground) must have chroma set to 0. Only accent colors like --primary, --destructive, and --chart-* should retain chroma.
For example, if shadcn generates:
.dark {
--background: oklch(0.13 0.028 261.692);
--card: oklch(0.21 0.034 264.665);
--muted: oklch(0.278 0.033 256.848);
--muted-foreground: oklch(0.707 0.022 261.325);
}
Post-process to:
.dark {
--background: oklch(0.13 0 0);
--card: oklch(0.18 0 0);
--muted: oklch(0.22 0 0);
--muted-foreground: oklch(0.65 0 0);
}
Variables to make monochrome (chroma=0, hue=0):
--background, --foreground, --card, --card-foreground, --popover, --popover-foreground, --secondary, --secondary-foreground, --muted, --muted-foreground, --accent, --accent-foreground, --border, --input, --ring, --sidebar, --sidebar-foreground, --sidebar-accent, --sidebar-accent-foreground
Variables to keep as-is (retain generated chroma):
--primary, --primary-foreground, --destructive, --chart-1 through --chart-5, --sidebar-primary, --sidebar-primary-foreground
After initialization, if an AGENTS.md file exists at the project root, verify it documents the Base UI and Phosphor icon conventions. The create-next skill creates this file with the correct conventions.
shadcn/ui supports multiple component registries beyond the official library. This enables access to specialized components from third-party sources.
React Bits - Animated and interactive components
npx shadcn add @react-bits/<component>ElevenLabs UI - Agent and audio components
npx shadcn add @elevenlabs-ui/<component>MapCN - Map components built on MapLibre
npx shadcn add @mapcn/<component>After running shadcn init, your components.json file supports namespace-based registries. Add custom registries to the configuration:
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "base-maia",
"registries": {
"@react-bits": {
"url": "https://reactbits.dev"
},
"@elevenlabs-ui": {
"url": "https://ui.elevenlabs.io"
},
"@mapcn": {
"url": "https://mapcn.dev"
}
}
}
Note: Most community registries work without explicit configuration - the CLI discovers them automatically. The registries section is only needed for private or custom registries.
# Official shadcn components
bunx shadcn@latest add button
# React Bits - animated components
bunx shadcn@latest add @react-bits/animated-card
# ElevenLabs - audio player
bunx shadcn@latest add @elevenlabs-ui/audio-player
# MapCN - interactive maps
bunx shadcn@latest add @mapcn/map-viewer
The shadcn MCP server enables AI-assisted component discovery and installation using natural language.
Setup:
# Install shadcn MCP server (if using Claude Code or compatible IDE)
# Configuration is typically added to your MCP settings
Usage: Ask your AI assistant to find and install components:
The MCP server works with:
.env.local)After running the shadcn init command, dark mode requires additional setup.
bun add next-themes
Create components/theme-provider.tsx:
"use client";
import { ThemeProvider as NextThemesProvider } from "next-themes";
type ThemeProviderProps = React.ComponentProps<typeof NextThemesProvider>;
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
}
Note: The preset includes the dropdown-menu component, so no additional installation is needed.
Create components/mode-toggle.tsx:
"use client";
import { Moon, Sun } from "@phosphor-icons/react";
import { useTheme } from "next-themes";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
export function ModeToggle() {
const { setTheme } = useTheme();
return (
<DropdownMenu>
<DropdownMenuTrigger className="inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 border border-input bg-background hover:bg-accent hover:text-accent-foreground h-10 w-10 shrink-0 cursor-pointer">
<Sun size={20} className="scale-100 rotate-0 transition-transform dark:scale-0 dark:-rotate-90" />
<Moon size={20} className="absolute scale-0 rotate-90 transition-transform dark:scale-100 dark:rotate-0" />
<span className="sr-only">Toggle theme</span>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => setTheme("light")}>
Light
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme("dark")}>
Dark
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme("system")}>
System
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}
Update app/layout.tsx (or src/app/layout.tsx):
import { Nunito_Sans } from "next/font/google";
import { ThemeProvider } from "@/components/theme-provider";
const nunitoSans = Nunito_Sans({ variable: "--font-sans" });
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" suppressHydrationWarning className={nunitoSans.variable}>
<body className="font-sans antialiased">
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
>
{children}
</ThemeProvider>
</body>
</html>
);
}
Font setup: The shadcn preset uses Nunito Sans with the
--font-sansCSS variable, which is referenced by the@theme inlineblock inglobals.css. Thefont-sansTailwind utility then uses this variable.
The ModeToggle component is now ready to use anywhere in your app. Example usage in a page:
Update app/page.tsx:
import { ModeToggle } from "@/components/mode-toggle";
export default function Home() {
return (
<div className="flex min-h-screen items-center justify-center bg-background">
<div className="text-center">
<h1 className="text-4xl font-bold text-foreground mb-4">
Dark Mode Setup Complete
</h1>
<p className="text-muted-foreground mb-8">
Click the button below to toggle themes
</p>
<ModeToggle />
</div>
</div>
);
}
suppressHydrationWarning to the <html> tagdisableTransitionOnChange to prevent jarring transitionsdefaultTheme="system" to respect OS preferenceDO NOT use hardcoded colors like:
bg-white, bg-zinc-50, text-zinc-900, border-zinc-200DO use semantic colors:
bg-background, bg-muted, text-foreground, border-borderThis skill uses Tailwind CSS v4. Some class names have changed from v3:
| Old (v3) | New (v4) | Usage |
|---|---|---|
bg-gradient-to-b | bg-linear-to-b | Linear gradients |
bg-gradient-to-r | bg-linear-to-r | Linear gradients |
bg-gradient-to-t | bg-linear-to-t | Linear gradients |
bg-gradient-to-l | bg-linear-to-l | Linear gradients |
Always use canonical v4 class names to avoid linter warnings.
| Semantic Class | Light Mode | Dark Mode |
|---|---|---|
bg-background | White | Dark gray |
bg-foreground | Dark text | Light text |
bg-card | White | Dark card |
bg-muted | Light gray | Dark gray |
bg-accent | Hover state | Hover state |
bg-primary | Brand color | Brand color |
text-foreground | Dark text | Light text |
text-muted-foreground | Gray text | Light gray text |
border-border | Light border | Dark border |
Hero section:
<section className="bg-linear-to-b from-muted/50 to-background">
Cards:
<div className="bg-card border border-border rounded-xl">
Text:
<h1 className="text-foreground">Title</h1>
<p className="text-muted-foreground">Description</p>
Buttons:
<button className="bg-primary text-primary-foreground hover:bg-primary/90">
When using shadcn with Base UI (not Radix), several component APIs differ. Base UI variants do not support asChild. Use these patterns instead:
render prop instead of asChild// WRONG - Base UI does not support asChild
<AlertDialogTrigger asChild>
<Button variant="ghost">Delete</Button>
</AlertDialogTrigger>
// CORRECT - use render prop
<AlertDialogTrigger
render={<Button variant="ghost" />}
>
Delete
</AlertDialogTrigger>
buttonVariants instead of <Button asChild>import { buttonVariants } from "@/components/ui/button";
import { cn } from "@/lib/utils";
// WRONG - asChild not supported
<Button asChild variant="outline" size="sm">
<a href={url} download>Download</a>
</Button>
// CORRECT - use buttonVariants utility
<a href={url} download className={cn(buttonVariants({ variant: "outline", size: "sm" }))}>
Download
</a>
DropdownMenuTrigger renders a button by default. Do NOT wrap it with <Button asChild> — this causes nested buttons and hydration errors.
// WRONG - causes hydration error
<DropdownMenuTrigger asChild>
<Button variant="outline" size="icon">...</Button>
</DropdownMenuTrigger>
// CORRECT - apply styles directly
<DropdownMenuTrigger className="inline-flex items-center justify-center ...">
...
</DropdownMenuTrigger>
onValueChange type safetyThe Base UI Slider onValueChange callback may return number | number[] depending on configuration. Always guard with Array.isArray:
<Slider
value={[currentValue]}
onValueChange={(val) => {
const num = Array.isArray(val) ? val[0] : val;
setValue(num);
}}
/>
bunx shadcn@latest add @react-bits/animated-card
import { AnimatedCard } from "@/components/ui/animated-card";
export function FeatureCard() {
return (
<AnimatedCard className="bg-card border border-border">
<h3 className="text-foreground font-bold">Feature Title</h3>
<p className="text-muted-foreground">Interactive animated card component</p>
</AnimatedCard>
);
}
bunx shadcn@latest add @elevenlabs-ui/audio-player
import { AudioPlayer } from "@/components/ui/audio-player";
export function VoicePreview() {
return (
<div className="bg-card border border-border rounded-xl p-4">
<AudioPlayer
src="/audio/sample.mp3"
className="text-foreground"
/>
</div>
);
}
bunx shadcn@latest add @mapcn/map-viewer
import { MapViewer } from "@/components/ui/map-viewer";
export function LocationMap() {
return (
<MapViewer
center={[-122.4194, 37.7749]}
zoom={12}
className="border border-border rounded-xl"
markers={[
{ lat: 37.7749, lng: -122.4194, label: "San Francisco" }
]}
/>
);
}
import { Button } from "@/components/ui/button"; // Official shadcn
import { AnimatedCard } from "@/components/ui/animated-card"; // React Bits
import { AudioPlayer } from "@/components/ui/audio-player"; // ElevenLabs UI
export function RichMediaCard() {
return (
<AnimatedCard className="bg-card border border-border p-6">
<h2 className="text-foreground text-2xl font-bold mb-4">Podcast Episode</h2>
<AudioPlayer src="/episodes/latest.mp3" />
<Button className="mt-4 bg-primary text-primary-foreground">
Subscribe
</Button>
</AnimatedCard>
);
}
globals.css contains html { @apply h-dvh overscroll-none; }.dark block in globals.css uses monochrome grays (chroma=0) for all neutral variablesnpx claudepluginhub mattwoodco/skills --plugin skillsProvides shadcn/ui expert guidance on CLI init/add, component installation, composition patterns, custom registries, theming, Tailwind CSS integration, and UI design. Use for setup, customization, or troubleshooting.
Manages shadcn/ui components and projects: adding components, styling with Tailwind, building forms, and composing design systems.
Installs, configures, and implements shadcn/ui React components with Tailwind CSS, React Hook Form, Zod for accessible UIs like buttons, forms, dialogs, tables.