Skip to Content
PatternsComponents

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

CVA Variants

class-variance-authority defines type-safe variant maps. Every visual variant is declarative, composable, and has defaults.

Radix + Slot

Radix UI primitives handle focus management, keyboard navigation, and screen reader behavior. Slot enables polymorphic rendering via asChild.

cn() Composition

clsx + tailwind-merge. Handles conditional classes and resolves Tailwind conflicts. Never use string concatenation for classNames.

data-slot Attributes

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.

Variant
Size
What gets rendered
<button data-slot="button" data-variant="default" data-size="default" className="...bg-primary text-primary-foreground..." > mukoko button </button>
Data attributes
data-slot="button"data-variant="default"data-size="default"

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.

components/ui/example-component.tsx
"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.

variant usage
// 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).

lib/utils.ts
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 className

Radix 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.

polymorphic rendering
// 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.

data attribute usage
// 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
Last updated on