Agent skill
shadcn-ui
shadcn/ui component patterns with Radix primitives and Tailwind styling. Use when building UI components, using CVA variants, implementing compound components, or styling with data-slot attributes. Triggers on shadcn, cva, cn(), data-slot, Radix, Button, Card, Dialog, VariantProps.
Install this agent skill to your Project
npx add-skill https://github.com/existential-birds/beagle/tree/main/plugins/beagle-react/skills/shadcn-ui
SKILL.md
shadcn/ui Component Development
Contents
- CLI Commands - Installing and adding components
- Quick Reference - cn(), basic CVA pattern
- Component Anatomy - Props typing, asChild, data-slot
- Component Patterns - Compound components
- Styling Techniques - CVA variants, modern CSS selectors, accessibility states
- Decision Tables - When to use CVA, compound components, asChild, Context
- Common Patterns - Form elements, dialogs, sidebars
- Reference Files - Full implementations and advanced patterns
CLI Commands
Initialize shadcn/ui
npx shadcn@latest init
This creates a components.json configuration file and sets up:
- Tailwind CSS configuration
- CSS variables for theming
- cn() utility function
- Required dependencies
Add Components
# Add a single component
npx shadcn@latest add button
# Add multiple components
npx shadcn@latest add button card dialog
# Add all available components
npx shadcn@latest add --all
Important: The package name changed in 2024:
- Old (deprecated):
npx shadcn-ui@latest add - Current:
npx shadcn@latest add
Common Options
-y, --yes- Skip confirmation prompt-o, --overwrite- Overwrite existing files-c, --cwd <cwd>- Set working directory--src-dir- Use src directory structure
Quick Reference
cn() Utility
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
Basic CVA Pattern
import { cva, type VariantProps } from "class-variance-authority"
const buttonVariants = cva(
"base-classes-applied-to-all-variants",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground",
outline: "border bg-background",
},
size: {
sm: "h-8 px-3",
lg: "h-10 px-6",
},
},
defaultVariants: {
variant: "default",
size: "sm",
},
}
)
function Button({
variant,
size,
className,
...props
}: React.ComponentProps<"button"> & VariantProps<typeof buttonVariants>) {
return (
<button
className={cn(buttonVariants({ variant, size }), className)}
{...props}
/>
)
}
export { Button, buttonVariants }
Component Anatomy
Props Typing Patterns
// HTML elements
function Component({ className, ...props }: React.ComponentProps<"div">) {
return <div className={cn("base-classes", className)} {...props} />
}
// Radix primitives
function Component({ className, ...props }: React.ComponentProps<typeof RadixPrimitive.Root>) {
return <RadixPrimitive.Root className={cn("base-classes", className)} {...props} />
}
// With CVA variants
function Component({
variant, size, className, ...props
}: React.ComponentProps<"button"> & VariantProps<typeof variants>) {
return <button className={cn(variants({ variant, size }), className)} {...props} />
}
asChild Pattern
Enables polymorphic rendering via @radix-ui/react-slot:
import { Slot } from "@radix-ui/react-slot"
function Button({
asChild = false,
className,
variant,
size,
...props
}: React.ComponentProps<"button"> & VariantProps<typeof buttonVariants> & { asChild?: boolean }) {
const Comp = asChild ? Slot : "button"
return (
<Comp
data-slot="button"
className={cn(buttonVariants({ variant, size }), className)}
{...props}
/>
)
}
Usage:
<Button>Click me</Button> // Renders <button>
<Button asChild><a href="/home">Home</a></Button> // Renders <a> with button styling
<Button asChild><Link href="/dash">Dash</Link></Button> // Works with Next.js Link
data-slot Attributes
Every component includes data-slot for CSS targeting:
function Card({ ...props }) { return <div data-slot="card" {...props} /> }
function CardHeader({ ...props }) { return <div data-slot="card-header" {...props} /> }
CSS/Tailwind targeting:
[data-slot="button"] { /* styles */ }
[data-slot="card"] [data-slot="button"] { /* nested targeting */ }
<div className="[&_[data-slot=button]]:shadow-lg">
<Button>Automatically styled</Button>
</div>
Conditional layouts with has():
<div
data-slot="card-header"
className={cn(
"grid gap-2",
"has-data-[slot=card-action]:grid-cols-[1fr_auto]"
)}
/>
Component Patterns
Compound Components
export { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter }
function Card({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card"
className={cn("bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm", className)}
{...props}
/>
)
}
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
return <div data-slot="card-header" className={cn("grid gap-2 px-6", className)} {...props} />
}
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
return <div data-slot="card-title" className={cn("leading-none font-semibold", className)} {...props} />
}
Styling Techniques
CVA Variants
Multiple dimensions:
const buttonVariants = cva("base-classes", {
variants: {
variant: {
default: "bg-primary text-primary-foreground",
destructive: "bg-destructive text-white",
outline: "border bg-background",
ghost: "hover:bg-accent",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2",
sm: "h-8 px-3",
lg: "h-10 px-6",
icon: "size-9",
},
},
defaultVariants: { variant: "default", size: "default" },
})
Compound variants:
compoundVariants: [
{ variant: "outline", size: "lg", class: "border-2" },
]
Type extraction:
type ButtonVariants = VariantProps<typeof buttonVariants>
// Result: { variant?: "default" | "outline" | ..., size?: "sm" | "lg" | ... }
Modern CSS Selectors in Tailwind
has() selector:
<button className="px-4 has-[>svg]:px-3"> // Adjusts padding when contains icon
<div className="has-data-[slot=action]:grid-cols-[1fr_auto]"> // Conditional layout
Group/peer selectors:
<div className="group" data-state="collapsed">
<div className="group-data-[state=collapsed]:hidden">Hidden when collapsed</div>
</div>
<button className="peer/menu" data-active="true">Menu</button>
<div className="peer-data-[active=true]/menu:text-accent">Styled when sibling active</div>
Container queries:
<div className="@container/card">
<div className="@md:flex-row">Responds to container width</div>
</div>
Accessibility States
className={cn(
// Focus
"outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
// Invalid
"aria-invalid:border-destructive aria-invalid:ring-destructive/20",
// Disabled
"disabled:pointer-events-none disabled:opacity-50",
)}
<span className="sr-only">Close</span> // Screen reader only
Dark Mode
Semantic tokens adapt automatically:
className="bg-background text-foreground dark:bg-input/30 dark:hover:bg-input/50"
Tokens: bg-background, text-foreground, bg-primary, text-primary-foreground, bg-card, text-card-foreground, border-input, text-muted-foreground
Decision Tables
When to Use CVA
| Scenario | Use CVA | Alternative |
|---|---|---|
| Multiple visual variants (primary, outline, ghost) | Yes | Plain className |
| Size variations (sm, md, lg) | Yes | Plain className |
| Compound conditions (outline + large = thick border) | Yes | Conditional cn() |
| One-off custom styling | No | className prop |
| Dynamic colors from props | No | Inline styles or CSS variables |
When to Use Compound Components
| Scenario | Use Compound | Alternative |
|---|---|---|
| Complex UI with multiple semantic parts | Yes | Single component with many props |
| Optional sections (header, footer) | Yes | Boolean show/hide props |
| Different styling for each part | Yes | CSS selectors |
| Shared state between parts | Yes + Context | Props drilling |
| Simple wrapper with children | No | Single component |
When to Use asChild
| Scenario | Use asChild | Alternative |
|---|---|---|
| Component should work as link or button | Yes | Duplicate component |
| Need button styles on custom element | Yes | Export variant styles |
| Integration with routing libraries | Yes | Wrapper components |
| Always renders same element | No | Standard component |
When to Use Context
| Scenario | Use Context | Alternative |
|---|---|---|
| Deep prop drilling (>3 levels) | Yes | Props |
| State shared by many siblings | Yes | Lift state up |
| Plugin/extension architecture | Yes | Props |
| Simple parent-child communication | No | Props |
Common Patterns
Form Input
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
return (
<input
type={type}
data-slot="input"
className={cn(
"h-9 w-full rounded-md border px-3 py-1",
"outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
"aria-invalid:border-destructive aria-invalid:ring-destructive/20",
"disabled:cursor-not-allowed disabled:opacity-50",
"placeholder:text-muted-foreground dark:bg-input/30",
className
)}
{...props}
/>
)
}
Dialog Content
function DialogContent({ children, showCloseButton = true, ...props }) {
return (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
data-slot="dialog-content"
className={cn(
"fixed top-[50%] left-[50%] translate-x-[-50%] translate-y-[-50%] w-full max-w-lg",
"bg-background border rounded-lg p-6 shadow-lg",
"data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95",
"data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95",
)}
{...props}
>
{children}
{showCloseButton && (
<DialogPrimitive.Close className="absolute top-4 right-4">
<XIcon /><span className="sr-only">Close</span>
</DialogPrimitive.Close>
)}
</DialogPrimitive.Content>
</DialogPortal>
)
}
Sidebar with Context
function SidebarProvider({ defaultOpen = true, children }) {
const isMobile = useIsMobile()
const [open, setOpen] = React.useState(defaultOpen)
React.useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === "b" && (e.metaKey || e.ctrlKey)) {
e.preventDefault()
setOpen(o => !o)
}
}
window.addEventListener("keydown", handleKeyDown)
return () => window.removeEventListener("keydown", handleKeyDown)
}, [])
const contextValue = React.useMemo(
() => ({ state: open ? "expanded" : "collapsed", open, setOpen, isMobile }),
[open, setOpen, isMobile]
)
return (
<SidebarContext.Provider value={contextValue}>
<div
data-slot="sidebar-wrapper"
style={{ "--sidebar-width": "16rem", "--sidebar-width-icon": "3rem" } as React.CSSProperties}
>
{children}
</div>
</SidebarContext.Provider>
)
}
Reference Files
For comprehensive examples and advanced patterns:
- components.md - Full implementations: Button, Card, Badge, Input, Label, Textarea, Dialog
- cva.md - CVA patterns: compound variants, responsive variants, type extraction
- patterns.md - Architectural patterns: compound components, asChild, controlled state, Context, data-slot, has() selectors
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
review-python
Comprehensive Python/FastAPI backend code review with optional parallel agents
review-verification-protocol
Mandatory verification steps for all code reviews to reduce false positives. Load this skill before reporting ANY code review findings.
sqlalchemy-code-review
Reviews SQLAlchemy code for session management, relationships, N+1 queries, and migration patterns. Use when reviewing SQLAlchemy 2.0 code, checking session lifecycle, relationship() usage, or Alembic migrations.
fastapi-code-review
Reviews FastAPI code for routing patterns, dependency injection, validation, and async handlers. Use when reviewing FastAPI apps, checking APIRouter setup, Depends() usage, or response models.
pytest-code-review
Reviews pytest test code for async patterns, fixtures, parametrize, and mocking. Use when reviewing test_*.py files, checking async test functions, fixture usage, or mock patterns.
postgres-code-review
Reviews PostgreSQL code for indexing strategies, JSONB operations, connection pooling, and transaction safety. Use when reviewing SQL queries, database schemas, JSONB usage, or connection management.
Didn't find tool you were looking for?