
Shadcn Ui
- 320 installs
- 74 repo stars
- Updated July 21, 2026
- existential-birds/beagle
shadcn-ui is a Claude Code skill that helps developers implement accessible shadcn/ui components with correct Radix primitives, Tailwind design tokens, variants, and form patterns aligned to a design system.
About
shadcn-ui is a frontend development skill for Claude Code that guides correct implementation of shadcn/ui components using Radix UI primitives, Tailwind CSS tokens, component variants, and accessible form patterns. The skill helps developers wire buttons, dialogs, selects, and form controls so they match project design system conventions instead of copying incomplete snippets. It focuses on accessibility defaults, variant APIs, and composition patterns common in React and Next.js codebases adopting shadcn/ui. Reach for shadcn-ui when you are adding or refactoring UI components and need Radix behavior, Tailwind styling, and form wiring done consistently.
- Component scaffolding
- Radix accessibility
- Tailwind variants
- Form + dialog patterns
- Theme token alignment
Shadcn Ui by the numbers
- 320 all-time installs (skills.sh)
- Ranked #731 of 2,245 Frontend Development skills by installs in the Skillselion catalog
- Data as of Jul 28, 2026 (Skillselion catalog sync)
npx skills add https://github.com/existential-birds/beagle --skill shadcn-uiAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 320 |
|---|---|
| repo stars | ★ 74 |
| Last updated | July 21, 2026 |
| Repository | existential-birds/beagle ↗ |
How do you implement shadcn/ui components correctly?
Implement accessible shadcn/ui components with correct Radix primitives, Tailwind tokens, variants, and form patterns aligned to your design system.
Who is it for?
React and Next.js developers adopting shadcn/ui who need Radix-backed components, Tailwind tokens, and accessible form patterns implemented correctly.
Skip if: Vue or Svelte-only projects, backend API work, or teams using Material UI or Chakra without shadcn/ui in the stack.
When should I use this skill?
The user asks to add, customize, or fix shadcn/ui components, Radix primitives, Tailwind variants, or form patterns in a React frontend.
What you get
Accessible shadcn/ui component implementations with Radix primitives, Tailwind variant classes, and form control patterns
- Component implementations
- Form pattern wiring
- Variant class definitions
Files
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 initThis 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 --allImportant: 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
Gates (CLI and file changes)
Run these in order before init / add (skip only when you are not running the CLI—e.g. copying snippets from this skill):
1. Working directory: Pass: pwd and package.json at that path identify the app root that should receive components/ and components.json (use -c <cwd> if the shell is elsewhere). 2. After `init`: Pass: components.json exists at that app root (or the documented path for your monorepo layout). 3. Before `--overwrite`: Pass: you can list which tracked files will be replaced, or version control shows the change is intentional and recoverable.
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 Linkdata-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 layoutGroup/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 onlyDark 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](./references/components.md) - Full implementations: Button, Card, Badge, Input, Label, Textarea, Dialog
- [cva.md](./references/cva.md) - CVA patterns: compound variants, responsive variants, type extraction
- [patterns.md](./references/patterns.md) - Architectural patterns: compound components, asChild, controlled state, Context, data-slot, has() selectors
shadcn/ui Component Reference
This document provides complete component implementations from shadcn/ui, demonstrating proper TypeScript typing, variant patterns, and data-slot usage.
Table of Contents
- Button Component
- Card Compound Component
- Badge Component
- Input Component
- Label Component
- Textarea Component
- Dialog Component
Button Component
The Button component demonstrates CVA variants, asChild polymorphism, and proper TypeScript typing.
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost:
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3",
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
icon: "size-9",
"icon-sm": "size-8",
"icon-lg": "size-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
function Button({
className,
variant,
size,
asChild = false,
...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}
/>
)
}
export { Button, buttonVariants }Key Features:
- CVA variants for
variantandsizeprops defaultVariantsspecify fallback valuesasChildpattern using@radix-ui/react-slotfor polymorphism- TypeScript:
React.ComponentProps<"button">+VariantProps<typeof buttonVariants> data-slot="button"for CSS targetinghas-[>svg]:px-3- modern CSS selector for conditional padding
Card Compound Component
The Card component family demonstrates the compound component pattern with multiple related components.
import * as React from "react"
import { cn } from "@/lib/utils"
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(
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
className
)}
{...props}
/>
)
}
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-title"
className={cn("leading-none font-semibold", className)}
{...props}
/>
)
}
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
}
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-action"
className={cn(
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
className
)}
{...props}
/>
)
}
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-content"
className={cn("px-6", className)}
{...props}
/>
)
}
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-footer"
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
{...props}
/>
)
}
export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardAction,
CardDescription,
CardContent,
}Key Features:
- Compound component pattern - each part is independently exported
- Each component has its own
data-slotattribute - Container queries:
@container/card-header - Advanced selectors:
has-data-[slot=card-action]:grid-cols-[1fr_auto] - Parent class selectors:
[.border-b]:pb-6 - Consistent
React.ComponentProps<"div">typing
Usage Example:
<Card>
<CardHeader>
<CardTitle>Title</CardTitle>
<CardDescription>Description</CardDescription>
<CardAction>
<Button>Action</Button>
</CardAction>
</CardHeader>
<CardContent>Content here</CardContent>
<CardFooter>Footer content</CardFooter>
</Card>Badge Component
The Badge component shows variant-based styling with asChild support.
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const badgeVariants = cva(
"inline-flex items-center justify-center rounded-full border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
{
variants: {
variant: {
default:
"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
secondary:
"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
destructive:
"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
},
},
defaultVariants: {
variant: "default",
},
}
)
function Badge({
className,
variant,
asChild = false,
...props
}: React.ComponentProps<"span"> &
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
const Comp = asChild ? Slot : "span"
return (
<Comp
data-slot="badge"
className={cn(badgeVariants({ variant }), className)}
{...props}
/>
)
}
export { Badge, badgeVariants }Key Features:
[a&]:hover:bg-primary/90- parent selector for link wrapperstransition-[color,box-shadow]- specific transition propertiesasChildpolymorphism for rendering as different elements- Single variant dimension with four options
Input Component
The Input component shows form element styling with focus and validation states.
import * as React from "react"
import { cn } from "@/lib/utils"
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
return (
<input
type={type}
data-slot="input"
className={cn(
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
className
)}
{...props}
/>
)
}
export { Input }Key Features:
file:*pseudo-element styling for file inputsplaceholder:*pseudo-class stylingselection:*pseudo-element for text selectionaria-invalid:*accessibility state stylingfocus-visible:*for keyboard navigation focus- Responsive text sizing:
text-base md:text-sm
Label Component
The Label component wraps Radix UI primitives with proper styling.
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { cn } from "@/lib/utils"
function Label({
className,
...props
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
return (
<LabelPrimitive.Root
data-slot="label"
className={cn(
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
className
)}
{...props}
/>
)
}
export { Label }Key Features:
- Wraps
@radix-ui/react-labelprimitive React.ComponentProps<typeof LabelPrimitive.Root>- type from primitivegroup-data-[disabled=true]:*- group state stylingpeer-disabled:*- sibling state styling
Textarea Component
The Textarea component demonstrates form element with field-sizing.
import * as React from "react"
import { cn } from "@/lib/utils"
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
return (
<textarea
data-slot="textarea"
className={cn(
"border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className
)}
{...props}
/>
)
}
export { Textarea }Key Features:
field-sizing-content- auto-grow textareamin-h-16with auto-expand capability- Same focus/validation states as Input
- Consistent form element styling patterns
Dialog Component
The Dialog component demonstrates Radix UI integration with compound components.
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { XIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function Dialog({
...props
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
return <DialogPrimitive.Root data-slot="dialog" {...props} />
}
function DialogTrigger({
...props
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
}
function DialogPortal({
...props
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
}
function DialogClose({
...props
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
}
function DialogOverlay({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
return (
<DialogPrimitive.Overlay
data-slot="dialog-overlay"
className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
className
)}
{...props}
/>
)
}
function DialogContent({
className,
children,
showCloseButton = true,
...props
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
showCloseButton?: boolean
}) {
return (
<DialogPortal data-slot="dialog-portal">
<DialogOverlay />
<DialogPrimitive.Content
data-slot="dialog-content"
className={cn(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
className
)}
{...props}
>
{children}
{showCloseButton && (
<DialogPrimitive.Close
data-slot="dialog-close"
className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
>
<XIcon />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
)}
</DialogPrimitive.Content>
</DialogPortal>
)
}
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="dialog-header"
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
{...props}
/>
)
}
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="dialog-footer"
className={cn(
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
className
)}
{...props}
/>
)
}
function DialogTitle({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
return (
<DialogPrimitive.Title
data-slot="dialog-title"
className={cn("text-lg leading-none font-semibold", className)}
{...props}
/>
)
}
function DialogDescription({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
return (
<DialogPrimitive.Description
data-slot="dialog-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
}
export {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogOverlay,
DialogPortal,
DialogTitle,
DialogTrigger,
}Key Features:
- Wraps multiple Radix UI Dialog primitives
data-[state=open]:*- animation based on primitive state- Portal for overlay rendering outside DOM hierarchy
- Optional
showCloseButtonprop with default value sr-onlyfor screen reader text- Center positioning with transform:
top-[50%] left-[50%] translate-x-[-50%] translate-y-[-50%]
Usage Example:
<Dialog>
<DialogTrigger asChild>
<Button>Open Dialog</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Dialog Title</DialogTitle>
<DialogDescription>Dialog description text.</DialogDescription>
</DialogHeader>
<div>Content goes here</div>
<DialogFooter>
<Button>Cancel</Button>
<Button>Confirm</Button>
</DialogFooter>
</DialogContent>
</Dialog>Class Variance Authority (CVA) Reference
This document covers CVA patterns used in shadcn/ui for variant-based styling with Tailwind CSS.
Basic CVA Pattern
import { cva, type VariantProps } from "class-variance-authority"
const componentVariants = cva(
// Base classes applied to all variants
"base-class-1 base-class-2",
{
variants: {
// Variant dimension name
variantName: {
// Variant option: classes
option1: "classes-for-option-1",
option2: "classes-for-option-2",
},
},
defaultVariants: {
variantName: "option1",
},
}
)
// Extract TypeScript types from the variant definition
type ComponentVariants = VariantProps<typeof componentVariants>Button Variants Example
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive: "bg-destructive text-white hover:bg-destructive/90",
outline: "border bg-background shadow-xs hover:bg-accent",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2",
sm: "h-8 rounded-md gap-1.5 px-3",
lg: "h-10 rounded-md px-6",
icon: "size-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)Usage in Component:
function Button({
className,
variant,
size,
...props
}: React.ComponentProps<"button"> &
VariantProps<typeof buttonVariants>) {
return (
<button
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
)
}Compound Variants
Compound variants apply classes when multiple variant conditions are met simultaneously.
const buttonVariants = cva(
"base-classes",
{
variants: {
variant: {
default: "bg-primary",
outline: "border",
},
size: {
sm: "h-8",
lg: "h-12",
},
disabled: {
true: "opacity-50",
false: "",
},
},
compoundVariants: [
{
// When variant=outline AND size=lg
variant: "outline",
size: "lg",
class: "border-2", // Use thicker border
},
{
// When variant=default AND disabled=true
variant: "default",
disabled: true,
class: "bg-primary/50", // Dim the background
},
],
defaultVariants: {
variant: "default",
size: "sm",
disabled: false,
},
}
)Sidebar Menu Button Variants
Real-world example with compound variants from Sidebar component:
const sidebarMenuButtonVariants = cva(
"peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-hidden ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50 group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2!",
{
variants: {
variant: {
default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
outline:
"bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]",
},
size: {
default: "h-8 text-sm",
sm: "h-7 text-xs",
lg: "h-12 text-sm group-data-[collapsible=icon]:p-0!",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)Key Features:
peer/menu-button- named peer for sibling selectorsgroup-data-[collapsible=icon]:size-8!- conditional sizing based on parent statetransition-[width,height,padding]- specific transition properties- Custom CSS variables:
hsl(var(--sidebar-border))
Default Variants
Default variants specify which variant options are used when no props are provided:
const badgeVariants = cva(
"inline-flex items-center rounded-full border px-2 py-0.5 text-xs font-medium",
{
variants: {
variant: {
default: "border-transparent bg-primary text-primary-foreground",
secondary: "border-transparent bg-secondary text-secondary-foreground",
destructive: "border-transparent bg-destructive text-white",
outline: "text-foreground",
},
},
defaultVariants: {
variant: "default", // Used when no variant prop provided
},
}
)
// Usage
<Badge /> // Uses variant="default"
<Badge variant="destructive" /> // Uses variant="destructive"Responsive Variants with Container Queries
CVA variants can include responsive modifiers and container query classes:
const cardVariants = cva(
"rounded-lg border p-4",
{
variants: {
layout: {
compact: "gap-2 @container/card:gap-4",
comfortable: "gap-4 @container/card:gap-6",
spacious: "gap-6 @container/card:gap-8",
},
responsive: {
true: "flex-col @md:flex-row",
false: "flex-col",
},
},
defaultVariants: {
layout: "comfortable",
responsive: true,
},
}
)Key Features:
@container/card:gap-4- container query modifier@md:flex-row- container breakpoint- Boolean variants for toggleable behavior
Integration with cn() Utility
CVA works seamlessly with the cn() utility for merging class names:
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
// In component
function Button({
className,
variant,
size,
...props
}: React.ComponentProps<"button"> & VariantProps<typeof buttonVariants>) {
return (
<button
className={cn(
buttonVariants({ variant, size }), // CVA output
className // User-provided overrides
)}
{...props}
/>
)
}How it works: 1. buttonVariants({ variant, size }) generates variant classes 2. User's className prop can override specific properties 3. clsx() conditionally combines classes 4. twMerge() intelligently merges Tailwind classes, resolving conflicts
Type Extraction
Extract TypeScript types from variant definitions:
import { type VariantProps } from "class-variance-authority"
const buttonVariants = cva("base", {
variants: {
variant: { default: "", destructive: "" },
size: { sm: "", md: "", lg: "" },
},
})
// Extract types for use in component props
type ButtonVariants = VariantProps<typeof buttonVariants>
// Result:
// {
// variant?: "default" | "destructive"
// size?: "sm" | "md" | "lg"
// }
// Use in component
interface ButtonProps extends React.ComponentProps<"button">, ButtonVariants {
asChild?: boolean
}Export Pattern
Always export both the component and the variants for reusability:
const buttonVariants = cva(/* ... */)
function Button({ ... }) {
// Implementation
}
// Export both for external use
export { Button, buttonVariants }Why export variants:
- Allows style reuse in other components
- Enables composition of variant styles
- Supports extending components with same styling
Example:
import { buttonVariants } from "@/components/ui/button"
function CustomButton() {
return (
<a
className={cn(
buttonVariants({ variant: "outline", size: "lg" }),
"custom-additional-classes"
)}
>
Link styled as button
</a>
)
}Advanced Pattern: Nullable Variants
Handle optional variant states with explicit null/undefined handling:
const alertVariants = cva(
"rounded-lg border p-4",
{
variants: {
severity: {
info: "border-blue-500 bg-blue-50",
warning: "border-yellow-500 bg-yellow-50",
error: "border-red-500 bg-red-50",
success: "border-green-500 bg-green-50",
},
dismissible: {
true: "pr-10",
false: "pr-4",
},
},
// No defaultVariants means undefined is valid
}
)
// TypeScript allows undefined
function Alert({
severity,
dismissible = false,
}: VariantProps<typeof alertVariants>) {
// severity can be undefined
return <div className={cn(alertVariants({ severity, dismissible }))} />
}Performance Considerations
CVA generates static classes that can be tree-shaken:
// Good: Variants are statically analyzable
const buttonVariants = cva("base", {
variants: {
variant: {
primary: "bg-blue-500",
secondary: "bg-gray-500",
},
},
})
// Avoid: Dynamic class generation loses tree-shaking
const dynamicButton = (color: string) => cn(`bg-${color}-500`)Best Practices:
- Define all variant options statically
- Use CVA for variant-based styling, not arbitrary values
- Leverage defaultVariants for sensible defaults
- Export variants for reusability
- Combine with
cn()for user overrides
shadcn/ui Component Patterns
This document covers architectural patterns used in shadcn/ui components.
Table of Contents
- Compound Component Pattern
- asChild / Slot Polymorphism
- Controlled vs Uncontrolled State
- Context for Complex Components
- data-slot CSS Targeting
- has() Selector Usage
Compound Component Pattern
Compound components split complex UI into multiple related components that work together.
Card Example
// Export multiple related components
export {
Card, // Container
CardHeader, // Header section
CardTitle, // Title element
CardDescription, // Description text
CardContent, // Main content area
CardFooter, // Footer section
CardAction, // Action area (optional)
}
// Usage - compose as needed
<Card>
<CardHeader>
<CardTitle>Dashboard</CardTitle>
<CardDescription>Overview of your account</CardDescription>
<CardAction>
<Button>Settings</Button>
</CardAction>
</CardHeader>
<CardContent>
Main content here
</CardContent>
<CardFooter>
Footer content
</CardFooter>
</Card>Key Benefits:
- Flexible composition - use only needed parts
- Clear semantic structure
- Each component handles its own styling
- Type-safe with independent prop types
Dialog Example
export {
Dialog, // Root component (state container)
DialogTrigger, // Opens the dialog
DialogContent, // Modal content wrapper
DialogHeader, // Header section
DialogTitle, // Title (accessibility required)
DialogDescription, // Description (accessibility)
DialogFooter, // Footer for actions
DialogClose, // Close button
}
// Usage
<Dialog>
<DialogTrigger asChild>
<Button>Open</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Confirm Action</DialogTitle>
<DialogDescription>This action cannot be undone.</DialogDescription>
</DialogHeader>
<DialogFooter>
<DialogClose asChild>
<Button variant="outline">Cancel</Button>
</DialogClose>
<Button>Confirm</Button>
</DialogFooter>
</DialogContent>
</Dialog>Sidebar Context Example
Complex compound components use Context for shared state:
// 1. Define context type
type SidebarContextProps = {
state: "expanded" | "collapsed"
open: boolean
setOpen: (open: boolean) => void
toggleSidebar: () => void
isMobile: boolean
}
// 2. Create context
const SidebarContext = React.createContext<SidebarContextProps | null>(null)
// 3. Custom hook for consuming context
function useSidebar() {
const context = React.useContext(SidebarContext)
if (!context) {
throw new Error("useSidebar must be used within a SidebarProvider.")
}
return context
}
// 4. Provider component
function SidebarProvider({ children, defaultOpen = true, ...props }) {
const [open, setOpen] = React.useState(defaultOpen)
const isMobile = useIsMobile()
const toggleSidebar = React.useCallback(() => {
setOpen((open) => !open)
}, [])
const contextValue = React.useMemo(
() => ({
state: open ? "expanded" : "collapsed",
open,
setOpen,
toggleSidebar,
isMobile,
}),
[open, setOpen, toggleSidebar, isMobile]
)
return (
<SidebarContext.Provider value={contextValue}>
{children}
</SidebarContext.Provider>
)
}
// 5. Child components consume context
function SidebarTrigger({ ...props }) {
const { toggleSidebar } = useSidebar()
return (
<Button onClick={toggleSidebar} {...props}>
Toggle
</Button>
)
}
// Usage
<SidebarProvider>
<Sidebar>
<SidebarHeader>Header</SidebarHeader>
<SidebarContent>Content</SidebarContent>
</Sidebar>
<SidebarTrigger />
</SidebarProvider>asChild / Slot Polymorphism
The asChild pattern allows components to render as different elements while preserving styling and behavior.
Basic Pattern
import { Slot } from "@radix-ui/react-slot"
function Button({
asChild = false,
className,
...props
}: React.ComponentProps<"button"> & { asChild?: boolean }) {
const Comp = asChild ? Slot : "button"
return (
<Comp
className={cn(buttonVariants({ className }))}
{...props}
/>
)
}Usage Examples
// Renders as <button>
<Button>Click me</Button>
// Renders as <a> with button styling
<Button asChild>
<a href="/home">Home</a>
</Button>
// Renders as Next.js Link
<Button asChild>
<Link href="/dashboard">Dashboard</Link>
</Button>
// Renders as custom component
<Button asChild>
<motion.div whileHover={{ scale: 1.05 }}>
Animated Button
</motion.div>
</Button>How Slot Works
The Slot component from Radix UI merges props and classes from the wrapper onto the child:
// With asChild=true
<Button asChild className="custom-class" onClick={handler}>
<a href="/home">Home</a>
</Button>
// Renders as:
<a
href="/home"
className="button-variant-classes custom-class"
onClick={handler}
>
Home
</a>Badge with asChild
function Badge({
asChild = false,
variant,
className,
...props
}: React.ComponentProps<"span"> &
VariantProps<typeof badgeVariants> &
{ asChild?: boolean }) {
const Comp = asChild ? Slot : "span"
return (
<Comp
data-slot="badge"
className={cn(badgeVariants({ variant }), className)}
{...props}
/>
)
}
// Usage
<Badge asChild variant="destructive">
<a href="/alerts">5 Alerts</a>
</Badge>Controlled vs Uncontrolled State
Components support both controlled (parent manages state) and uncontrolled (internal state) patterns.
Uncontrolled Pattern
function Checkbox({ defaultChecked = false, ...props }) {
const [checked, setChecked] = React.useState(defaultChecked)
return (
<input
type="checkbox"
checked={checked}
onChange={(e) => setChecked(e.target.checked)}
{...props}
/>
)
}
// Usage - component manages own state
<Checkbox defaultChecked={true} />Controlled Pattern
function Checkbox({ checked, onCheckedChange, ...props }) {
return (
<input
type="checkbox"
checked={checked}
onChange={(e) => onCheckedChange?.(e.target.checked)}
{...props}
/>
)
}
// Usage - parent controls state
const [checked, setChecked] = useState(false)
<Checkbox checked={checked} onCheckedChange={setChecked} />Hybrid Pattern (Sidebar)
Support both controlled and uncontrolled usage:
function SidebarProvider({
defaultOpen = true,
open: openProp,
onOpenChange: setOpenProp,
...props
}) {
// Internal state
const [_open, _setOpen] = React.useState(defaultOpen)
// Use prop if provided, otherwise internal state
const open = openProp ?? _open
const setOpen = React.useCallback(
(value: boolean | ((value: boolean) => boolean)) => {
const openState = typeof value === "function" ? value(open) : value
// Call prop callback if provided
if (setOpenProp) {
setOpenProp(openState)
} else {
// Otherwise update internal state
_setOpen(openState)
}
},
[setOpenProp, open]
)
return (
<SidebarContext.Provider value={{ open, setOpen }}>
{children}
</SidebarContext.Provider>
)
}
// Uncontrolled usage
<SidebarProvider defaultOpen={false}>
<Sidebar />
</SidebarProvider>
// Controlled usage
const [sidebarOpen, setSidebarOpen] = useState(true)
<SidebarProvider open={sidebarOpen} onOpenChange={setSidebarOpen}>
<Sidebar />
</SidebarProvider>Context for Complex Components
Use React Context for components with multiple children sharing state.
When to Use Context
- Multiple child components need access to shared state
- Props drilling would be excessive
- State logic is complex (e.g., Sidebar open/collapsed/mobile states)
- Component has plugin/extension architecture
Form Context Example
type FormContextValue = {
formId: string
errors: Record<string, string>
register: (name: string) => void
unregister: (name: string) => void
}
const FormContext = React.createContext<FormContextValue | null>(null)
function useFormContext() {
const context = React.useContext(FormContext)
if (!context) {
throw new Error("Form components must be used within Form")
}
return context
}
function Form({ children, onSubmit }: FormProps) {
const [errors, setErrors] = React.useState({})
const formId = React.useId()
const contextValue = React.useMemo(
() => ({
formId,
errors,
register: (name) => { /* ... */ },
unregister: (name) => { /* ... */ },
}),
[formId, errors]
)
return (
<FormContext.Provider value={contextValue}>
<form id={formId} onSubmit={onSubmit}>
{children}
</form>
</FormContext.Provider>
)
}
function FormField({ name, ...props }) {
const { formId, errors, register } = useFormContext()
React.useEffect(() => {
register(name)
return () => unregister(name)
}, [name])
return (
<div>
<input id={`${formId}-${name}`} {...props} />
{errors[name] && <span>{errors[name]}</span>}
</div>
)
}Best Practices
1. Memoize context value to prevent unnecessary re-renders:
const contextValue = React.useMemo(
() => ({ state, setState, helpers }),
[state, setState, helpers]
)2. Type the context properly:
const Context = React.createContext<ContextType | null>(null)3. Provide helpful error messages:
if (!context) {
throw new Error("useComponent must be used within ComponentProvider")
}4. Use custom hooks for consuming context:
function useComponent() {
const context = React.useContext(ComponentContext)
if (!context) throw new Error("...")
return context
}data-slot CSS Targeting
Every shadcn/ui component includes a data-slot attribute for CSS targeting.
Basic Usage
function Button({ ...props }) {
return <button data-slot="button" {...props} />
}
function Card({ ...props }) {
return <div data-slot="card" {...props} />
}CSS Targeting:
/* Target all buttons */
[data-slot="button"] {
/* styles */
}
/* Target buttons within cards */
[data-slot="card"] [data-slot="button"] {
/* styles */
}Tailwind Usage:
<div className="[&_[data-slot=button]]:shadow-lg">
<Button>Styled via parent</Button>
</div>Advanced Patterns
Conditional Layouts Based on Slots
function CardHeader({ className, ...props }) {
return (
<div
data-slot="card-header"
className={cn(
"grid gap-2 px-6",
// If CardAction is present, use two columns
"has-data-[slot=card-action]:grid-cols-[1fr_auto]",
className
)}
{...props}
/>
)
}
// Usage
<CardHeader>
<CardTitle>Title</CardTitle>
{/* When CardAction is added, layout changes automatically */}
<CardAction>
<Button>Action</Button>
</CardAction>
</CardHeader>Parent Selectors with Slots
function CardFooter({ className, ...props }) {
return (
<div
data-slot="card-footer"
className={cn(
"flex items-center px-6",
// If parent has .border-t class, add top padding
"[.border-t]:pt-6",
className
)}
{...props}
/>
)
}
// Usage
<Card className="border-t">
<CardFooter>Footer gets top padding</CardFooter>
</Card>Multiple data-* Attributes
Components can have multiple data attributes for different purposes:
function Sidebar({ variant, side, collapsible, ...props }) {
return (
<div
data-slot="sidebar"
data-variant={variant}
data-side={side}
data-collapsible={collapsible}
className={cn(
"group",
"group-data-[variant=floating]:rounded-lg",
"group-data-[side=left]:border-r",
"group-data-[collapsible=icon]:w-12"
)}
{...props}
/>
)
}has() Selector Usage
Modern CSS :has() selector enables parent styling based on children.
Basic Pattern
// Button with icon adjusts padding
<button className="px-4 has-[>svg]:px-3">
<Icon />
Text
</button>How it works:
has-[>svg]- if button has direct child<svg>- Apply
px-3instead of basepx-4 - Automatically adjusts based on content
Size Variants with Icons
const buttonVariants = cva("...", {
variants: {
size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3",
sm: "h-8 px-3 has-[>svg]:px-2.5",
lg: "h-10 px-6 has-[>svg]:px-4",
},
},
})
// Padding adjusts automatically
<Button size="sm">
<Icon />
Text
</Button>CardHeader Grid Layout
function CardHeader({ className, ...props }) {
return (
<div
className={cn(
"grid gap-2",
// Single column by default
"grid-rows-[auto_auto]",
// When CardAction exists, add second column
"has-data-[slot=card-action]:grid-cols-[1fr_auto]",
className
)}
{...props}
/>
)
}
// Layout adapts based on children
<CardHeader>
<CardTitle>Title</CardTitle>
<CardDescription>Description</CardDescription>
{/* Adding this changes layout to two columns */}
<CardAction>
<Button>Action</Button>
</CardAction>
</CardHeader>Conditional Border Padding
function CardFooter({ className, ...props }) {
return (
<div
className={cn(
"flex items-center px-6",
// Add padding only if parent has border-t class
"[.border-t]:pt-6",
className
)}
{...props}
/>
)
}Group-based Conditional Styling
function SidebarGroupLabel({ className, ...props }) {
return (
<div
className={cn(
"flex h-8 items-center px-2",
// Hide when sidebar is collapsed (icon mode)
"group-data-[collapsible=icon]:-mt-8",
"group-data-[collapsible=icon]:opacity-0",
className
)}
{...props}
/>
)
}
// Parent controls child visibility
<div className="group" data-collapsible="icon">
<SidebarGroupLabel>Hidden in icon mode</SidebarGroupLabel>
</div>Peer-based Interactions
function SidebarMenuButton({ size, isActive, className, ...props }) {
return (
<button
data-slot="sidebar-menu-button"
data-size={size}
data-active={isActive}
className={cn(
"peer/menu-button",
"flex items-center gap-2",
className
)}
{...props}
/>
)
}
function SidebarMenuAction({ showOnHover, className, ...props }) {
return (
<button
data-slot="sidebar-menu-action"
className={cn(
"absolute right-1",
// Show when peer (menu button) is hovered
showOnHover && "peer-hover/menu-button:opacity-100 md:opacity-0",
// Highlight when peer is active
"peer-data-[active=true]/menu-button:text-accent-foreground",
className
)}
{...props}
/>
)
}
// Usage
<div>
<SidebarMenuButton isActive={true}>Menu</SidebarMenuButton>
<SidebarMenuAction showOnHover />
</div>Browser Support
The :has() selector is supported in all modern browsers (Chrome 105+, Safari 15.4+, Firefox 121+). For older browsers, provide fallback styling:
className={cn(
// Fallback for browsers without :has() support
"px-4",
// Modern browsers with :has() support
"has-[>svg]:px-3",
)}Related skills
FAQ
What stack does shadcn-ui assume?
shadcn-ui targets React frontends using shadcn/ui components built on Radix UI primitives and Tailwind CSS, including variant APIs and form patterns common in Next.js design systems.
When should developers use the shadcn-ui skill?
shadcn-ui fits when adding or refactoring UI components in a shadcn/ui codebase and the developer needs correct Radix behavior, Tailwind token usage, and accessible form wiring rather than generic CSS advice.