TL;DR
The web grew from documents into applications, and the way we think about building for it has to change too. Component-driven architecture inverts page-based thinking: instead of starting from whole pages and working down, you build up from reusable, self-contained units.
Core principles:
The art of programming is the art of organizing complexity, of mastering multitude and avoiding its bastard chaos as effectively as possible.
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 and its own place in the sitemap. Templates helped us share headers and footers across pages, but the page was still the basic unit we built with.
Then we started building applications, though that change didn’t happen all at once. Gmail in 2004 hinted at what was possible, and single-page applications followed. React arrived in 2013 and changed how most teams built interfaces. It wasn’t the first component library, but it made thinking in components easy to pick up and practical for large apps. Today, whether you use React, Vue, Svelte, Angular, or web components, you’re working with components in a way that a developer from 2005 wouldn’t recognize.
The tools changed, and so did the way we think about building for the web.
The Page Mindset
It’s worth looking at the page model first, because the component model grew out of its limits.
In page-based development, you think top-down. You start with the page and its layout, and work downward into the details. Each page is a destination, complete and self-contained, and the question you keep coming back to is “What does this page need to do?”
In this model, templates are there so you can reuse code, rather than to create an abstraction. You’d move the header into a partial because you don’t want to repeat yourself, and you wouldn’t be thinking of “Header” as a concept that exists independently of any particular page. The header partial exists to serve the pages that include it.
Server-side frameworks reinforced this thinking. Rails and Django both organized code around routes and views, where the URL determined which controller ran and which template was rendered, so everything flowed from the page.
In a lot of ways this model works well. Pages map cleanly to URLs, which means they also map cleanly to how users think about navigation, and each one is easy to reason about by itself. They match the way designers often deliver mockups, one page at a time. They also line up with how the web actually works at the protocol level, where you request a URL and get a document back.
But the page mindset doesn't cope well with complexity. As your interfaces become more interactive and the same UI elements appear in different contexts, thinking in pages starts to get in the way.
The Component Mindset
Component-driven architecture flips that mental model around. Instead of thinking top-down from pages to parts, you think bottom-up from parts to compositions.
A component is a self-contained piece of interface with its own structure and behavior. It has a clear boundary: inputs such as props and slots go in, and outputs such as rendered UI and events come out. 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 fit together?”
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, such as its own state for which item is active and its own logic for how it behaves on mobile. As a concept, it exists before any page uses it.
A component is also different from a template. A template is a hole where content goes, while a component is a self-sufficient unit that can contain other self-sufficient units. A template partial exists to serve its parent page, but a component is independent, so you can use it wherever its purpose fits.
Because components are independent, you can think at different levels of abstraction at the same time. A button is a component, and so are the card containing it, a list of those cards, and the page that combines that list with a header and a sidebar. Each one works by itself 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, and pages. The metaphor borrows from chemistry, where small things combine into larger things according to predictable rules.
The Atomic Hierarchy
Each level has different concerns and changes at a different rate: atoms stay stable, while pages change all the time.
- Atoms
- Atoms are the basic building blocks, such as buttons and inputs, which you can't break down any further at this level of abstraction.
- Molecules
- Molecules are simple combinations of atoms working together, such as a search form that combines an input field with a submit button.
- Organisms
- Organisms are more complex compositions that form distinct sections of interface, like a header containing a logo and navigation.
- Templates
- Templates set out the structure of a page with placeholder content, so they give you the layout without the real data.
- Pages
- Pages are templates filled in with real content, and they're what users actually see.
The metaphor isn’t perfect, because chemistry has fixed rules and component composition doesn’t, but the hierarchy it describes is real. You design from the bottom up, but you use what you’ve built from the top down.
The names you give the levels 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 or just “small components and big components.”
Boundaries and Contracts
Components need clear boundaries, because without them they start to depend on each other’s internals and you lose the benefits of encapsulation.
A component’s boundary is the line between its inside and its outside. Inside the boundary, the component has full control over its state and its implementation details. Outside the boundary, other components can only interact with it through its defined interfaces. They can’t get at its internals, and they don’t need to.
The boundary is enforced by a contract, which is 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. You can think of the contract as a promise: “Give me these inputs, and I’ll behave in these ways.”
Good contracts are narrow. Each extra prop adds coupling, and each piece of context the component needs is another dependency that makes it harder for you to reuse and test.
This is the principle of minimal knowledge: components should know as little as possible about the world beyond their boundary. When a component needs something, it’s better for you to pass it in explicitly than to have the component go and fetch it for itself. You can spot explicit dependencies and test them, but implicit ones are hard to find, which makes the component harder to understand and easier to break.
A button component needs to know its label and what to do when it’s clicked. It doesn’t need to know what page it’s on or which user is logged in.
Components can still access global state or context, and sometimes that’s the right choice, but it should be a decision you make deliberately and visibly, 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 with an icon and sometimes without, how do you model that?
Two Approaches to Variation
Configuration Approach
You add a prop for each variant:
<Button
variant="primary"
size="large"
icon="save"
iconPosition="left"
>
Save
</Button>The component ends up as a Swiss Army knife, with a growing API that tries to handle every possible case.
Composition Approach
You 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. With configuration, the combinations multiply with each prop you add: if you have three sizes and four variants and two icon positions, you have twenty-four combinations to implement and test within a single component. Composition keeps complexity local, because each building block handles its own concerns and the combinations come from how you put the blocks together.
Composition also copes with cases you didn’t plan for. A heavily configured component can only do what you expected when you wrote it, but with composition, the people using your component can build combinations you never imagined.
The compound component pattern is a good example of this. 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 take this further. A modal component doesn’t need header and footer props, because it can accept children and render them in the right places. A card doesn’t need title and actions props when it can accept CardHeader and CardBody children. The component defines the structure, and whoever uses it fills in the content.
Props aren't bad, and simple, stable variations often work best as props. Once the variations start to multiply, though, composition is usually the better choice, before your 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, and nothing outside the component can access it.
Local state is good because it’s encapsulated 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 and be confident it’ll just work.
Derived state is state calculated from other state, such as a filtered list derived from a full list and a search term, or a total derived from line items. These values don’t need their own storage, because you can compute them from the source data whenever you need them. If you store derived state separately, you can end up with synchronization bugs when the copies drift apart, but computing it keeps a single source of truth.
It gets harder when state needs to be shared. When two components need to react to the same data, that data has to be stored somewhere both can access. This is where component thinking runs into application architecture, and it’s where a lot of us struggle.
It’s tempting to lift state high, putting it at the root so it’s available everywhere. That works, but it creates hidden dependencies, because components that look self-contained actually depend on the structure of that global state. If you change that structure, you might break components scattered across your codebase.
In component-driven architecture, state is kept as close as possible to where it's used. You only lift state when you need to share it, and then only as far as 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, meaning data that comes from the server and belongs to it, follows different rules. Your app caches it locally, but the source of truth is somewhere else. Modern data-fetching patterns are built around this distinction, and treat server state as a cache to keep in sync rather than as application state to manage. When you keep the two apart, it’s easier to work out what’s local, what’s shared within this client, and what belongs to the server.
Thinking in Components
Adopting component-driven architecture takes more than using a component framework, because it changes how you look at interfaces.
When you look at a design mockup, do you see pages or building blocks? When you review code, do you check whether the page works or whether each component is coherent?
I’ve found that once you have the component mindset, you look at every interface as a composition, and a complex screen becomes something to break down into parts. The hard part is usually finding the right component boundaries, rather than building the screen itself.
Breaking a screen down like this is design work, even if it doesn’t look like traditional design. The decisions you make about where component boundaries fall and what contracts they expose have a large effect on how easy your system is to maintain 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 gives you components that are either too granular, each doing so little that you need many of them to get anything done, or too monolithic, doing everything with a 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?
Each one helps you find a different kind of problem. If explaining a component’s purpose needs an “and”, such as “it displays user data and handles form submission”, the component is doing too much. If you need to mock half the application to test a component, its dependencies are too broad, and if styling changes break its behavior, its concerns aren’t separated.
In component-driven development, refactoring often comes down to breaking components apart. When one of your components gets too complex, the usual fix is to split it into several smaller ones, each with a simpler job.
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 and reactivity models differ, the underlying way of thinking is the same in all of them.
If you’re a React developer who thinks in components, you can follow how a Vue codebase is put together, even if the code itself looks unfamiliar. A Svelte developer can sketch component boundaries on a whiteboard in a way that a React developer immediately recognizes.
This helps a lot as frameworks come and go, and when you change jobs and pick up a new stack. You still need to learn each framework’s syntax, but thinking in components is the part you bring with you.
Beyond the Browser
Component thinking has spread beyond frontend development: design tools like Figma now have component systems, and mobile development has adopted similar compositional models.
That’s not a coincidence, because component architecture solves a problem that comes up everywhere: building complex things from simpler things without losing coherence. Encapsulated units with clear boundaries and explicit contracts, which you can combine into larger wholes, work wherever you’re building interfaces.
The page didn't disappear, but it's no longer the basic unit of construction. Now it's an assembly point, 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, and it comes down to six ideas:
- Bottom-up thinking, from building blocks to compositions instead of top-down from pages to parts
- Complete encapsulation, with clear boundaries between component internals and external interfaces
- Composition over configuration, so you handle variation by combining parts instead of adding configuration
- Local state, lifted only when you need to share it
- Design work at every level, because component boundaries are architectural decisions that shape how easy your code is to maintain
- Thinking that transfers 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 gives us that model, and it’s a way of thinking about how complex interfaces get built and maintained over time that works whichever framework you use.
Related Content:
- Foundations: The Progressive Enhancement Mindset, Accessibility as a Foundation
- Guides: Choosing a State Management Strategy, Building with Progressive Enhancement
- Patterns: Compound Component, Composition over Inheritance, Component Interface, Component State, Controlled vs Uncontrolled Components
External Resources:
- Atomic Design by Brad Frost - Brad Frost’s book on the atomic design methodology
- Thinking in React - React’s guide to breaking a UI into components
- Web Components - The browser’s own component model
- Component-Driven Development - Methodology for building UIs from components up