Skip to main content
Saved
Foundation

Component-Driven Architecture

How frontend development moved from page-based thinking to component-based thinking.

Den Odell
By Den Odell Added
In this article
  1. TL;DR
  2. The Shift from Documents to Applications
  3. The Page Mindset
  4. The Component Mindset
  5. The Atomic Metaphor
  6. Small to Medium
  7. Medium to Large
  8. Boundaries and Contracts
  9. Composition Over Configuration
  10. Configuration Approach
  11. Composition Approach
  12. State and Behavior
  13. Thinking in Components
  14. Questions That Reveal Problems
  15. Components Across Frameworks
  16. Beyond the Browser
  17. Summary
TL;DR

TL;DR

The web evolved from documents to applications, and our mental model must evolve with it. Component-driven architecture inverts page-based thinking, building from reusable, self-contained units rather than top-down pages.

Core principles:

Think bottom-up: build from components, not pages
Clear boundaries: encapsulate structure, behavior, and styling
Composition over configuration: combine simple parts rather than configure complex ones
Minimal knowledge: components should know as little as possible about the outside world

The art of programming is the art of organizing complexity, of mastering multitude and avoiding its bastard chaos as effectively as possible.

— Edsger W. Dijkstra

The Shift from Documents to Applications

The web started with documents. HTML was designed to mark up text with headings, paragraphs, lists, and links, and pages linked to other pages. The browser was a document viewer, and we were all publishers.

That mental model served us well for a surprisingly long time. We built websites as collections of pages, each one a distinct document with its own URL, its own content, its own place in the sitemap. Templates helped us share headers and footers across pages, but the fundamental unit of construction remained the page itself.

Then we started building applications, and that shift wasn’t sudden. Gmail in 2004 hinted at what was possible. Single-page applications emerged. React arrived in 2013 and changed how most teams built interfaces. It wasn’t the first component library, but it made component thinking accessible and practical at scale. Today, whether you use React, Vue, Svelte, Angular, or web components, you’re working within a component paradigm that would be unrecognizable to a developer from 2005.

The tools changed, and so did the way we think about building for the web.

The Page Mindset

The page model is worth looking at first, because the component model grew out of its limits.

In page-based development, you think top-down. You start with the page, its purpose, its layout, its content, and work downward into the details. Each page is a destination, a complete unit that stands alone. The question you ask is: “What does this page need to do?”

Templates in this model are about code reuse, not abstraction. You extract the header into a partial because you don’t want to repeat yourself, not because you’re modeling a “Header” concept that exists independently of any particular page. The header partial exists to serve pages, not the other way around.

Server-side frameworks reinforced this thinking. Rails, Django, PHP: they all organized code around routes and views. The URL determined which controller ran, which template rendered, which page the user saw. Everything flowed from the page.

This model works well in a lot of ways. Pages map cleanly to URLs, which means they map cleanly to how users think about navigation. They’re easy to reason about in isolation. They match how designers often work, delivering page-by-page mockups. And they align with how the web actually works at the protocol level: you request a URL, you get a document.

But the page mindset struggles with complexity. As interfaces become more interactive, as the same UI elements appear in different contexts, as applications need to feel responsive rather than document-like, thinking in pages starts to get in the way.

The Component Mindset

Component-driven architecture inverts the mental model. Instead of thinking top-down from pages to parts, you think bottom-up from parts to compositions.

A component is a self-contained unit of interface that encapsulates its own structure, behavior, and often its own styling. It has a clear boundary, receiving inputs (props, attributes, slots) and producing outputs (rendered UI, events, callbacks). Because you can understand and test it in isolation, you can compose it with other components to build larger structures.

Instead of asking “what does this page need?”, you ask “what are the building blocks, and how do they compose?”

This change goes further than it first seems. When you think in components, you stop seeing a navigation bar as 'the thing at the top of the page' and start seeing it as an independent piece of UI that happens to appear at the top of pages.

The navigation bar has its own concerns: its own state (which item is active), its own logic (what happens on mobile), its own API (what links does it display). It exists conceptually before any page uses it.

A component is also different from a template. A template says “here’s a hole where content goes.” A component says “here’s a self-sufficient unit that can contain other self-sufficient units.” A template partial exists to serve its parent page, while a component stands on its own and can be used wherever its purpose fits.

Because components stand on their own, you can think at different levels of abstraction at the same time. A button is a component. A card containing that button is a component. A list of those cards is a component. A page composing that list with a header and sidebar is a component. Each one works on its own and also as part of the level above it.

The Atomic Metaphor

Brad Frost’s Atomic Design gave us a useful vocabulary for this hierarchy: atoms, molecules, organisms, templates, pages. The metaphor borrows from chemistry, where small things combine into larger things according to predictable rules.

The Atomic Hierarchy

Small to Medium

Atoms are the fundamental building blocks, irreducible at this level of abstraction: buttons, inputs, and labels.

Molecules are simple combinations of atoms working together, such as a search form that combines an input field with a submit button.

Organisms are more complex compositions that form distinct sections of interface, like a header containing a logo, navigation, and search functionality.

Medium to Large

Templates define page-level structure with placeholder content, establishing the layout without the actual data.

Pages are templates populated with real content, which is what users see.

Each level has different concerns and different rates of change, with atoms being stable and pages being volatile.

The metaphor is imperfect (chemistry has fixed rules while component composition is arbitrary) but the hierarchy it describes is real. You design from the bottom up but use from the top down.

The specific terms are less important than the idea underneath them: complex interfaces are compositions of simpler parts. That’s true whether you call them atoms and molecules, primitives and compounds, or just “small components and big components.”

Boundaries and Contracts

Components need boundaries, because without clear boundaries they bleed into each other and the benefits of encapsulation disappear.

A component’s boundary is the line between its inside and its outside. Inside the boundary, the component has full control over its structure, its state, its implementation details. Outside the boundary, other components interact with it only through defined interfaces, unable to reach inside and with no need to do so.

This boundary is enforced by a contract: the component’s API. Props define what data flows in, events define what signals flow out, and slots define where child content can be injected. The contract is a promise: “Give me these inputs, and I’ll behave in these ways.”

Good contracts are narrow. Every additional prop is additional coupling. Every piece of context the component requires is a dependency that makes it harder to reuse and harder to test.

The principle here is minimal knowledge. Components should know as little as possible about the world beyond their boundary, and when a component needs something, prefer passing it in explicitly over having the component reach out to get it. Explicit dependencies are visible, testable, and controllable. Implicit ones are hard to see, which makes the component harder to understand and easier to break.

A button component needs to know its label and what to do when clicked. It doesn’t need to know what page it’s on, what user is logged in, or what color scheme the application uses.

This doesn’t mean components can never access global state or context, and sometimes that’s the right choice. It means that kind of access should be a deliberate, visible decision, with self-sufficiency as the default.

Composition Over Configuration

How should components handle variation? When you need a button that’s sometimes primary and sometimes secondary, sometimes large and sometimes small, sometimes with an icon and sometimes without, how do you model that?

Two Approaches to Variation

Configuration Approach

Add props for every variant:

<Button
  variant="primary"
  size="large"
  icon="save"
  iconPosition="left"
>
  Save
</Button>

The component becomes a Swiss Army knife, handling every possible case through a growing API.

Composition Approach

Build larger components from smaller ones:

<PrimaryButton>
  <SaveIcon />
  Save
</PrimaryButton>

Variation comes from combining different building blocks rather than configuring a single complex block.

Composition scales better than configuration. Configuration props multiply combinatorially: if you have three sizes and four variants and two icon positions, you have twenty-four combinations to design, implement, and test within a single component. Composition keeps complexity local. Each building block handles its own concerns, and combinations emerge naturally.

Composition also handles unforeseen cases. A heavily configured component can only do what its author anticipated. A compositional approach lets consumers build combinations the component author never imagined.

The compound component pattern is a good example. Instead of a select component that takes an array of options as a prop, you have a Select that composes Option children. Instead of a tabs component configured with an array of tab definitions, you have Tabs composing Tab and TabPanel children. The parent provides shared context and the children provide the content, so the way you arrange the children is the API.

Slots and children extend this further. A modal component doesn’t need header, body, and footer props. It can accept children and render them in the right places. A card doesn’t need title and subtitle and actions props. It can accept CardHeader, CardBody, and CardFooter children. The component defines the structure and consumers fill in the content.

This doesn't mean props are bad. Simple, stable variations often belong as props. Once the variations start to multiply, composition is usually the better choice, before the component ends up with dozens of configuration props.

State and Behavior

Components need to manage state, but how much state, and of what kind?

Local state is state that belongs to a single component instance: a dropdown’s open/closed status, an input’s current value before form submission, a counter’s count, or an accordion’s expanded sections. This state is internal to the component, invisible to the outside world, and managed entirely within the component’s boundary.

Local state is good because it’s encapsulated, testable, and doesn’t create coupling. A component that manages its own local state is self-sufficient: changes to that state don’t ripple through the application, and you can drop that component anywhere with confidence that it will just work.

Derived state is state calculated from other state. A filtered list derived from a full list and a search term. A total derived from line items. These values don’t need their own storage because they’re computed on demand from the source data. Storing derived state separately leads to synchronization bugs when the copies drift apart, while computing it keeps a single source of truth.

The challenge comes when state needs to be shared. When two components need to react to the same data, that data has to live somewhere both can access. This is where component thinking runs into application architecture, and where many teams struggle.

The temptation is to lift state high, to put it at the root and make it available everywhere. This works but creates hidden dependencies. Components that look self-contained actually depend on global state structures. Change the shape of that state, and you break components scattered across your codebase.

In component-driven architecture, state lives as close as possible to where it's used. Only lift state when necessary for sharing, and only to the nearest common ancestor of the components that need it.

Global state is sometimes necessary, but every piece of it couples together components that would otherwise be independent.

Server state, data that comes from and belongs to the server, follows different rules. It’s cached locally, yes, but the source of truth is elsewhere. Modern data-fetching patterns recognize this distinction, treating server state as a cache to be synchronized rather than application state to be managed. Keeping the two apart makes it easier to answer three questions: what’s local, what’s shared within this client, and what belongs to the server?

Thinking in Components

Adopting component-driven architecture means changing how you see interfaces, which takes more than using a component framework.

When you look at a design mockup, do you see pages or building blocks? When you plan a feature, do you think about what screens to build or what components to create? When you review code, do you ask whether the page works or whether each component is coherent?

I’ve found that the component mindset sees every interface as a composition, so a complex screen becomes something to break down into parts. The hard part is usually finding the right components, boundaries, and contracts, more than implementing the screen itself.

This decomposition is design work, even if it doesn’t look like traditional design. Deciding where component boundaries fall, how components communicate, what contracts they expose: these decisions have a large effect on how maintainable the system is over time.

Good decomposition is fractal: you can zoom in to any component and find it composed of smaller components, each with its own clear purpose. Bad decomposition creates components that are either too granular (doing almost nothing, requiring many to accomplish anything) or too monolithic (doing everything, with tangled internal structure).

Questions That Reveal Problems

I’ve learned to find the right granularity mostly through practice, but I keep coming back to the same questions:

  • Does this component have a single, clear purpose? Can I explain what it does in one sentence?
  • Could someone use it without understanding its implementation? Is the API self-evident?
  • Would I be comfortable publishing it as a library? Is it self-contained?

The answers reveal problems. If explaining a component’s purpose requires “and” (“it displays user data and handles form submission and manages the modal state”), the component is doing too much. If testing a component requires mocking half the application, its dependencies are too broad. If changing a component’s styling breaks its behavior, its concerns aren’t separated.

Refactoring in component-driven development often means decomposition. The usual fix for a component that’s too complex is to split it into several components, each with simpler responsibilities.

Components Across Frameworks

The component mental model also transfers between frameworks. React, Vue, Svelte, Angular, Solid, and web components all work with components, and while the syntax, reactivity models, and specific patterns differ, the underlying way of thinking is the same in all of them.

A React developer thinking in components can understand a Vue codebase conceptually, even if the code looks unfamiliar. A Svelte developer can sketch component boundaries on a whiteboard in a way that a React developer immediately recognizes.

That helps when frameworks come and go, when teams get acquired and merge codebases, and when developers change jobs and meet new stacks. You still need to know a specific framework’s syntax, but thinking in components is the part that comes with you.

Beyond the Browser

Component thinking has spread beyond frontend development, with design tools like Figma now featuring component systems, backend development increasingly using component-like patterns for UI rendering, and mobile development adopting similar compositional models.

That’s not a coincidence, because component architecture answers a problem that comes up everywhere: how do you build complex things from simpler things while maintaining coherence? The answer, encapsulated units with clear boundaries and explicit contracts that compose into larger wholes, applies wherever you’re building interfaces.

The page didn't disappear, but it's no longer the basic unit of construction. It's now an assembly point, a place where components come together to serve a purpose.

Pages still exist because users need somewhere to go, but we build them out of components.

Summary

Component-driven architecture changes how we think about building for the web:

  1. Think bottom-up from building blocks to compositions, not top-down from pages to parts
  2. Encapsulate completely with clear boundaries between component internals and external interfaces
  3. Compose over configure to handle variation through combination rather than configuration
  4. Keep state local and lift only when necessary for sharing
  5. Design at every level because component boundaries are architectural decisions that shape maintainability
  6. Transfer thinking across tools because the component mindset works in every framework

The page-based web served us well, but today’s interfaces need a different mental model. Component-driven architecture is that model: a way of thinking about how complex interfaces are built, maintained, and changed over time, rather than a feature of any one framework.


Related Content:

External Resources:

Newsletter

A Monthly Email
from Den Odell

Behind-the-scenes thinking on frontend patterns, site updates, and more

No spam. Unsubscribe anytime.