Skip to content

Compositional UI

The fractal nature of component-based interfaces

From Pages to Composition

Early web development treated pages as monolithic units—each page built largely from scratch, with reuse happening through copy-paste or server-side includes. Modern UI development inverts this: pages are compositions of components, which are themselves compositions of smaller components. The page becomes a delivery mechanism, not the organizing unit.

This shift is now table stakes. The interesting questions are: how do we think about decomposition, and where do we draw component boundaries?

Two Perspectives on the Same Insight

Two influential frameworks approach this question from opposite directions but arrive at the same core insight.

Atomic Design: Bottom-Up Naming

Brad Frost's Atomic Design provides a vocabulary for the levels of UI abstraction:

  • Atoms: Basic elements that can't be broken down further (buttons, inputs, labels)
  • Molecules: Simple combinations of atoms working as a unit (a search form: label + input + button)
  • Organisms: Complex components built from molecules and atoms (a header with navigation, search, and user menu)
  • Templates: Page-level layouts showing content structure
  • Pages: Templates populated with real content

The framework's key insight isn't the five levels—it's that UI exists at multiple levels of abstraction simultaneously, and we need language to talk about that.

Thinking in React: Top-Down Decomposition

React's Thinking in React approaches from the opposite direction: given a design, how do you break it into components?

The guidance is practical:

  1. Single responsibility: A component should do one thing. If it grows, decompose it.
  2. Data alignment: Component structure should mirror data structure. "Separate your UI into components, where each component matches one piece of your data model."
  3. Draw boxes: Literally draw rectangles around distinct UI sections—each box is a candidate component.

Both frameworks arrive at the same place: UI is hierarchical, components compose into larger components, and the same principles apply at every level.

The Fractal Pattern

Components are fractal—the same composition principles apply whether you're building a button or a page.

A ProductCard contains a ProductImage, ProductTitle, and AddToCartButton. The AddToCartButton contains an icon and text. The page contains a ProductGrid which contains ProductCards. At every level, the pattern is the same: smaller components compose into larger ones.

This is what Brad Frost means when he says atomic design is "not a linear process, but rather a mental model." You don't build atoms, then molecules, then organisms in sequence. You work at all levels simultaneously, as designer Frank Chimero describes: "a dance of switching contexts" between detailed component work and assessing how those components function within the broader composition.

The fractal nature means:

  • Principles don't change with scale: Single responsibility applies to atoms and organisms alike
  • Patterns repeat: A well-designed molecule and a well-designed organism look structurally similar
  • You can zoom in and out: Any level of the hierarchy can be understood in isolation or in context

Finding Component Boundaries

The practical question: when should something be its own component?

Single Responsibility

The clearest signal. If you can't describe what a component does without using "and," consider splitting it.

tsx
// Too many responsibilities
function ProductPage() {
  // fetches product data AND
  // manages cart state AND
  // renders product details AND
  // renders related products AND
  // handles reviews
}

// Separated concerns
function ProductPage() {
  return (
    <>
      <ProductDetails />
      <AddToCart />
      <RelatedProducts />
      <ProductReviews />
    </>
  );
}

Data Structure Alignment

When your component hierarchy mirrors your data model, the code becomes easier to reason about. If your API returns:

json
{
  "product": {
    "title": "...",
    "images": [...],
    "pricing": { "price": 99, "discount": 10 }
  }
}

Your components likely map to that structure: Product, ProductImages, ProductPricing.

Reusability Signals

If you find yourself wanting the same UI in multiple places, that's a component. But don't extract preemptively—wait until you actually need the reuse. Premature abstraction creates components that don't quite fit anywhere.

Complexity Isolation

When a piece of UI has complex internal logic (state management, effects, event handling), extracting it into a component isolates that complexity. The parent component doesn't need to know the details.

tsx
// DateRangePicker encapsulates significant complexity
function ReportFilters() {
  return (
    <div>
      <DateRangePicker onChange={setDateRange} />
      <StatusFilter onChange={setStatus} />
    </div>
  );
}

The Granularity Tension

There's a real tension between granular components (maximum reusability, single responsibility) and comprehensible code (fewer files, less indirection).

Over-Decomposition Costs

  • Indirection overhead: Understanding the code requires jumping between many files
  • Prop drilling: Data passes through many layers
  • Abstraction friction: Components too generic to be useful without configuration

Under-Decomposition Costs

  • Duplication: Similar UI reimplemented in multiple places
  • Monolithic components: Hard to test, hard to modify, hard to understand
  • Mixed concerns: Changes to one feature risk breaking another

Finding the Balance

Some heuristics:

  • Delay extraction: Wait until you have two or three instances before extracting a reusable component
  • Colocate by default: Keep related code together until there's a reason to separate
  • Consider the reader: Would a new team member understand this structure?
  • Watch for pain: Difficulty testing, duplicated bugs, or fear of modification are signals to restructure

The right granularity depends on context. A design system warrants very granular atomic components. A single-use admin page might reasonably have larger, less reusable components.

Composition Over Configuration

One final principle: prefer composition over configuration.

tsx
// Configuration approach: flexible but opaque
<Card
  title="Product"
  subtitle="Description"
  image={productImage}
  actions={[{ label: 'Buy', onClick: handleBuy }]}
  variant="featured"
/>

// Composition approach: explicit and flexible
<Card variant="featured">
  <CardImage src={productImage} />
  <CardContent>
    <CardTitle>Product</CardTitle>
    <CardDescription>Description</CardDescription>
  </CardContent>
  <CardActions>
    <Button onClick={handleBuy}>Buy</Button>
  </CardActions>
</Card>

The composition approach is more verbose but more explicit. You can see the structure. You can easily modify it—add a badge, reorder elements, conditionally render sections. The configuration approach requires the component to anticipate every variation.

This doesn't mean configuration is always wrong—a Button component with variant="primary" is perfectly reasonable. But when components become Christmas trees of props, composition usually serves better.

Summary

  • UI is fractal: components compose into components at every level
  • Atomic Design names the levels; Thinking in React provides decomposition heuristics
  • Component boundaries follow from single responsibility, data alignment, and reuse
  • Balance granularity against comprehension—both extremes have costs
  • Prefer composition over configuration for flexibility

The component is the organizing unit of modern UI. Thinking compositionally—understanding UI as hierarchies of nested components—is the foundational mental model for building interfaces that scale.

Further Reading