Appearance
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:
- Single responsibility: A component should do one thing. If it grows, decompose it.
- Data alignment: Component structure should mirror data structure. "Separate your UI into components, where each component matches one piece of your data model."
- 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
- Atomic Design by Brad Frost—The full methodology and rationale
- Thinking in React—React's official guide to component decomposition
- Composition vs Inheritance—Why React favors composition