Component Architecture
Define the component hierarchy, file structure, prop interfaces, and composition patterns before writing implementation code.
Component architecture is the structural plan for the component library. Defined before implementation begins, it answers: what components exist, how they relate to each other, what their interfaces look like, and how they compose into pages. Without this plan, teams duplicate components, create incompatible APIs, and accumulate a component library that nobody fully understands.
Component Taxonomy
Organise components into four layers based on their level of abstraction and reusability. This is based on Atomic Design but with practical, engineering-oriented naming.
| Layer | Also called | Description | Examples |
|---|---|---|---|
| Primitives | Atoms | Smallest building blocks. No business logic. Fully generic and reusable. | Button, Input, Badge, Tooltip, Spinner |
| Composites | Molecules | Combinations of primitives with shared behaviour. Still generic. | SearchInput, FormField, DataTable, Combobox |
| Sections | Organisms | Feature-specific blocks with business context. Not reusable across features. | UserProfileCard, ProjectTaskList, InvoiceSummary |
| Templates | Templates | Page-level layout compositions. Slot-based, content-agnostic. | DashboardLayout, AuthLayout, SettingsLayout |
A component's layer is determined by whether it knows about your application's domain. A Button has no idea what a "project" is — it is a Primitive. A ProjectStatusBadge maps project status strings to visual variants — it is a Section.
File Structure
Each component should live in its own directory, co-located with its tests, stories, and types. This keeps related code together and makes deleting a component clean.
components/
ui/ # Primitives
button/
button.tsx # Component implementation
button.test.tsx # Unit tests
index.ts # Barrel export
input/
badge/
layout/ # Layout templates
dashboard-layout/
auth-layout/
[feature]/ # Feature-specific sections
projects/
project-card/
project-card.tsx
project-card.test.tsx
index.ts
project-task-list/Prop Interface Design
Props are the public API of a component. Design them with the same care as an HTTP API. Once a component is used across the codebase, changing its prop interface is a breaking change.
Prop design rules
- Prefer specific, typed props over generic
config: objectprops - Use union types for variants:
variant: "primary" | "secondary" | "ghost" - Always extend the native HTML element's props where applicable (
React.ButtonHTMLAttributes) - Never accept raw style objects as props — use variant tokens instead
- Use
childrenfor content slots, notlabelprops for complex content - Make components controlled by default (accept
value+onChange)
import { type ButtonHTMLAttributes, forwardRef } from "react"
import { cva, type VariantProps } from "class-variance-authority"
const buttonVariants = cva(
// Base styles (always applied)
"inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
outline: "border border-input hover:bg-accent",
ghost: "hover:bg-accent hover:text-accent-foreground",
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
},
size: {
sm: "h-8 px-3 text-xs",
default: "h-9 px-4",
lg: "h-10 px-8",
icon: "h-9 w-9 p-0",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
interface ButtonProps
extends ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
isLoading?: boolean
asChild?: boolean
}
const Button = forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, isLoading, children, ...props }, ref) => (
<button
ref={ref}
className={buttonVariants({ variant, size, className })}
disabled={isLoading || props.disabled}
aria-busy={isLoading}
{...props}
>
{isLoading ? <span className="animate-spin mr-2">↻</span> : null}
{children}
</button>
)
)
Button.displayName = "Button"
export { Button, buttonVariants }
export type { ButtonProps }Composition Patterns
Compound components
For components with multiple related parts, use the compound component pattern. This gives consumers explicit control over each part while keeping the internal logic encapsulated.
// Usage — consumer decides the structure
<Card>
<Card.Header>
<Card.Title>Project Alpha</Card.Title>
<Card.Description>Active since January 2024</Card.Description>
</Card.Header>
<Card.Content>
<MetricsGrid />
</Card.Content>
<Card.Footer>
<Button variant="outline">View Details</Button>
</Card.Footer>
</Card>
// vs. a single-prop approach that locks in the structure:
// <Card title="..." description="..." footer={<Button>} /> ← avoidRender props and slots
Use render props when a component needs to provide data to its children without dictating their structure. This is common in headless component patterns.
<Combobox
options={projects}
value={selected}
onChange={setSelected}
renderOption={(project) => (
<div className="flex items-center gap-2">
<StatusDot status={project.status} />
<span>{project.name}</span>
<span className="ml-auto text-xs text-muted-foreground">{project.code}</span>
</div>
)}
/>AI agents can generate a complete component skeleton from a specification. Define the prop interface, variant shape, and composition pattern, then prompt the agent to produce the component file, test file, barrel export, and a usage example. Agents follow established patterns well — use an existing component in your codebase as a style reference in the prompt. Review the generated prop types and edge case handling before merging.
Naming Conventions
| Item | Convention | Example |
|---|---|---|
| Component file | PascalCase | UserProfileCard.tsx |
| Component export | PascalCase | export function UserProfileCard() |
| Props interface | ComponentNameProps | UserProfileCardProps |
| Hook | use + camelCase | useProjectData |
| Context | ContextNameContext | AuthContext |
| Event handler prop | on + PascalCase | onStatusChange |
| Boolean prop | is/has/can + adjective | isLoading, hasError, canEdit |
| Barrel export | index.ts | components/ui/button/index.ts |
Component Specification
Before implementing a component, write a specification that answers these questions. This takes 15 minutes and saves hours of back-and-forth during review.
## Component: ProjectStatusBadge
### Purpose
Display a project's status with consistent visual treatment across all views.
### Variants
- status: "active" | "paused" | "completed" | "archived"
### States
- Default
- (No loading or error state — data is passed as prop)
### Accessibility
- Role: img (with aria-label describing the status)
- Not keyboard focusable (decorative status indicator)
### Props
| Prop | Type | Required | Description |
|--------|---------------|----------|--------------------------|
| status | ProjectStatus | Yes | Current project status |
| size | "sm" \| "md" | No | Badge size, default: md |
### Acceptance criteria
- AC-01: Each status maps to a distinct color and label
- AC-02: Label is readable at 200% browser zoom
- AC-03: Color is not the only differentiator (label + shape)Design System Governance
Evolve your design system without breaking products — token versioning, component deprecation, change logs, and patterns for managing systems across multiple products.
Prototyping & Validation
Build and test interface hypotheses cheaply before committing to implementation. The goal is to discover problems at the lowest possible cost.