Component Patterns
Every component in the registry follows these mandatory patterns: CVA for variants, Radix UI for accessibility, cn() for class composition, and data attributes for CSS targeting.
The 4 pillars
class-variance-authority defines type-safe variant maps. Every visual variant is declarative, composable, and has defaults.
Radix UI primitives handle focus management, keyboard navigation, and screen reader behavior. Slot enables polymorphic rendering via asChild.
clsx + tailwind-merge. Handles conditional classes and resolves Tailwind conflicts. Never use string concatenation for classNames.
Every component gets data-slot, data-variant, and data-size attributes for CSS targeting without fragile class selectors.
Live demonstration
Interactive demo showing how CVA variants, cn() composition, and data attributes work together. Toggle variants and inspect the rendered output.
<button
data-slot="button"
data-variant="default"
data-size="default"
className="...bg-primary text-primary-foreground..."
>
mukoko button
</button>These attributes are always present on the rendered element, enabling stable CSS targeting regardless of class name changes.
Full component anatomy
This is the complete pattern every registry component follows. Use this as your template when adding new components.
"use client"
import { cva, type VariantProps } from "class-variance-authority"
import * as Slot from "radix-ui/internal/slot"
import { cn } from "@/lib/utils"
// 1. Define variants with CVA
const exampleVariants = cva(
// Base classes — always applied
"inline-flex items-center justify-center rounded-lg font-medium transition-colors",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/80",
outline: "border border-border bg-input/30 hover:bg-input/50",
ghost: "hover:bg-muted hover:text-foreground",
},
size: {
default: "h-9 px-4 text-sm",
sm: "h-8 px-3 text-xs",
lg: "h-10 px-5 text-base",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
// 2. Define props — extend CVA variants + HTML attributes
interface ExampleProps
extends React.ComponentProps<"div">,
VariantProps<typeof exampleVariants> {
asChild?: boolean // Enable polymorphic rendering
}
// 3. Named export (never default export)
export function Example({
className,
variant = "default",
size = "default",
asChild = false,
...props
}: ExampleProps) {
// 4. Slot for polymorphic rendering
const Comp = asChild ? Slot.Root : "div"
return (
<Comp
// 5. data attributes for CSS targeting
data-slot="example"
data-variant={variant}
data-size={size}
// 6. cn() for class composition — never string concat
className={cn(exampleVariants({ variant, size, className }))}
{...props}
/>
)
}
// 7. Export variants for external use
export { exampleVariants }CVA variant system
class-variance-authority provides type-safe variant maps. Variants are composable — combine any variant + size combination. Default variants are applied automatically.
// Type-safe — TypeScript catches invalid variants
<Button variant="default" size="sm" />
<Button variant="outline" size="lg" />
<Button variant="ghost" /> // uses defaultVariants.size
// Use buttonVariants() for non-Button elements
<a className={cn(buttonVariants({ variant: "link" }))}>
Click here
</a>
// Extend with className — cn() resolves conflicts
<Button className="rounded-full bg-[var(--color-cobalt)]">
Custom
</Button>cn() composition
cn() is clsx + tailwind-merge. It handles conditional classes and resolves Tailwind conflicts (e.g., if both px-4 and px-2 are applied, the last one wins).
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
// Usage examples:
cn("px-4 py-2", "px-2")
// → "py-2 px-2" (px-4 removed, px-2 wins)
cn("text-red-500", isActive && "text-blue-500")
// → "text-blue-500" when isActive is true
cn(buttonVariants({ variant: "outline" }), className)
// → merges CVA output with custom classNameRadix UI + Slot polymorphism
The asChild prop with Radix Slot lets you render any element while keeping the component’s styles and behavior. This is how a Button can be rendered as a link.
// Renders as <button>
<Button>Click me</Button>
// Renders as <a> with Button styles
<Button asChild>
<a href="/dashboard">Go to dashboard</a>
</Button>
// Renders as Next.js Link with Button styles
<Button asChild>
<Link href="/patterns">View patterns</Link>
</Button>
// How it works internally:
function Button({ asChild = false, ...props }) {
const Comp = asChild ? Slot.Root : "button"
return <Comp {...props} />
// Slot.Root renders the child element with merged props
}Data attributes
Every component includes data attributes for stable CSS targeting. These are more reliable than class selectors which can change.
// Components output these attributes:
<button
data-slot="button"
data-variant="outline"
data-size="sm"
>
// Target in CSS (globals.css or Tailwind)
[data-slot="button"] { ... }
[data-slot="button"][data-variant="destructive"] { ... }
// Target in Tailwind with has-data-* modifier
<div className="has-data-[slot=button]:p-4">
<Button>Inside a container</Button>
</div>
// Useful for parent-based styling
.form-field [data-slot="label"] {
font-weight: 600;
}Component checklist
- CVA for all visual variants — never inline conditional classes
- cn() for all className composition — never string concatenation
- Radix UI primitives for accessibility (focus, keyboard, screen reader)
- data-slot attribute on the root element
- data-variant and data-size attributes when applicable
- Named exports only — no default exports
- “use client” only when using hooks, event handlers, or browser APIs
- All colors from CSS custom properties — no hardcoded hex
- Entry in registry.json with dependencies and files