| name | composition-patterns |
| description | React composition patterns for scalable component architecture. Use when building compound components, variant-driven primitives, or refactoring prop-heavy components. |
React Composition Patterns
CVA (Class Variance Authority) Pattern
Every UI primitive follows this contract:
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/cn";
const variants = cva("base-classes", {
variants: {
variant: { primary: "...", secondary: "...", ghost: "..." },
size: { sm: "...", md: "...", lg: "..." },
},
defaultVariants: { variant: "primary", size: "md" },
});
interface Props
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof variants> {}
export function Component({ className, variant, size, ref, ...props }: Props & { ref?: React.Ref<HTMLButtonElement> }) {
return <button ref={ref} className={cn(variants({ variant, size }), className)} {...props} />;
}
Rules:
VariantProps<typeof variants> for type-safe variant inference
- Extend native HTML attributes for full prop forwarding
cn() (tailwind-merge + clsx) as the ONLY class merging utility
className always last in cn() for consumer overrides
Compound Component Pattern
For complex UI with multiple sub-elements:
export function Card({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
return <div className={cn("rounded-lg border bg-card", className)} {...props} />;
}
export function CardHeader({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
return <div className={cn("flex flex-col space-y-1.5 p-6", className)} {...props} />;
}
export function CardTitle({ className, ...props }: React.HTMLAttributes<HTMLHeadingElement>) {
return <h3 className={cn("text-2xl font-semibold tracking-tight", className)} {...props} />;
}
export function () {
;
}
Rules:
- Each sub-component is independently exported (not nested)
- All accept
className for override support
- No internal state coupling between sub-components
- Barrel-export from
index.ts
Polymorphic Components
For components that render different HTML elements:
type HeadingLevel = "h1" | "h2" | "h3" | "h4" | "h5" | "h6";
interface HeadingProps extends React.HTMLAttributes<HTMLHeadingElement> {
as?: HeadingLevel;
}
export function Heading({ as: Tag = "h2", className, ...props }: HeadingProps) {
return <Tag className={cn("font-bold tracking-tight", className)} {...props} />;
}
Section Data/View/Composition Pattern
Every landing page section follows three layers:
*.data.ts → typed static content (title, description, CTA text)
*-view.tsx → pure presentation (JSX, Tailwind, animations)
*-section.tsx → composition (imports data + view, public API)
index.ts → barrel export
Rules:
- View receives typed props, never imports data directly
- Section is the only public export
- Data files are plain objects, no hooks or JSX
- View is
"use client" only when it uses interactivity
- Section stays as Server Component
Slot Pattern (className forwarding)
When wrapping native elements, always forward className:
export function Container({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
return <div className={cn("max-w-6xl mx-auto px-4 sm:px-6 lg:px-8", className)} {...props} />;
}
Anti-Patterns
- Boolean prop explosion (
showX, hideY, isZ)
- God components (500+ lines with mixed concerns)
- Inline style objects in JSX
- Passing
any as props
- Components that import their own data (tight coupling)
- Missing
className forwarding on wrapper components