Add shadcn
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".From its SKILL.md
npx -y skills add mattwoodco/skills --skill add-shadcnAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 3 stars3 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
15.8 KB, as published. Nobody here has run it
Quick Start
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.
Remove Demo Component
The shadcn preset includes a demo component-example.tsx file. Remove it:
rm -f src/components/component-example.tsx components/component-example.tsx
Update globals.css
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.
Enforce Monochrome Dark Mode Grays (Required)
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.
Multi-Registry Configuration
shadcn/ui supports multiple component registries beyond the official library. This enables access to specialized components from third-party sources.
Recommended Registries
React Bits - Animated and interactive components
- URL: https://reactbits.dev/
- Installation:
npx shadcn add @react-bits/<component>
ElevenLabs UI - Agent and audio components
- URL: https://ui.elevenlabs.io/
- Installation:
npx shadcn add @elevenlabs-ui/<component>
MapCN - Map components built on MapLibre
- URL: https://mapcn.dev/
- Installation:
npx shadcn add @mapcn/<component>
Configure Custom Registries
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.
Using Components from Different 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
MCP Server for Natural Language Component Discovery
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:
- "Find me a login form from the shadcn registry"
- "Install an animated card component from React Bits"
- "Add a map component for displaying locations"
The MCP server works with:
- Official shadcn/ui registry
- Third-party component libraries (React Bits, ElevenLabs UI, MapCN, etc.)
- Private company registries (requires authentication via
.env.local)
Dark Mode Setup (Required)
After running the shadcn init command, dark mode requires additional setup.
1. Install Dependencies
bun add next-themes
2. Create ThemeProvider Component
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>;
}
3. Create ModeToggle Component
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>
);
}
4. Update Root Layout
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.
5. Use the ModeToggle Component
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>
);
}
Critical Requirements
Prevent Flash of Unstyled Content (FOUC)
- Always add
suppressHydrationWarningto the<html>tag - Use
disableTransitionOnChangeto prevent jarring transitions - Use
defaultTheme="system"to respect OS preference
Use Semantic Color Classes
DO NOT use hardcoded colors like:
bg-white,bg-zinc-50,text-zinc-900,border-zinc-200
DO use semantic colors:
bg-background,bg-muted,text-foreground,border-border
Tailwind CSS v4 Canonical Classes
This 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.
Color Token Reference
| 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 |
Common Patterns
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">
Base UI Compatibility Notes
When using shadcn with Base UI (not Radix), several component APIs differ. Base UI variants do not support asChild. Use these patterns instead:
Triggers: use 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>
Styled links: use 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: apply styles directly
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>
Slider onValueChange type safety
The 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);
}}
/>
Practical Examples
Example 1: Animated Landing Page Card (React Bits)
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>
);
}
Example 2: Audio Player for Voice Content (ElevenLabs UI)
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>
);
}
Example 3: Interactive Map (MapCN)
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" }
]}
/>
);
}
Mixing Components from Multiple Registries
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>
);
}
Testing Checklist
- Toggle works (Light/Dark/System)
- Theme persists after page refresh
- No white flash on page load
- No hydration errors in console
- All text readable in both modes
- All borders visible in both modes
- Components from different registries work together
- Registry namespaces resolve correctly
-
globals.csscontainshtml { @apply h-dvh overscroll-none; } -
.darkblock in globals.css uses monochrome grays (chroma=0) for all neutral variables