Design Engineering
Design & Architecture

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.

LayerAlso calledDescriptionExamples
PrimitivesAtomsSmallest building blocks. No business logic. Fully generic and reusable.Button, Input, Badge, Tooltip, Spinner
CompositesMoleculesCombinations of primitives with shared behaviour. Still generic.SearchInput, FormField, DataTable, Combobox
SectionsOrganismsFeature-specific blocks with business context. Not reusable across features.UserProfileCard, ProjectTaskList, InvoiceSummary
TemplatesTemplatesPage-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.

Component file structure
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: object props
  • 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 children for content slots, not label props for complex content
  • Make components controlled by default (accept value + onChange)
Well-designed Button prop interface
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.

Compound component pattern — Card
// 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>} /> ← avoid

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

Render prop pattern — Combobox
<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

ItemConventionExample
Component filePascalCaseUserProfileCard.tsx
Component exportPascalCaseexport function UserProfileCard()
Props interfaceComponentNamePropsUserProfileCardProps
Hookuse + camelCaseuseProjectData
ContextContextNameContextAuthContext
Event handler propon + PascalCaseonStatusChange
Boolean propis/has/can + adjectiveisLoading, hasError, canEdit
Barrel exportindex.tscomponents/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 spec template
## 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)

On this page