Frontend16 min readJun 2026 Last verified Jun 2026
Component Architecture & Design Systems
How to scale a UI codebase without it rotting: composition over configuration, presentational vs container split, prop boundaries, design tokens, and a reusable component library.
Kill the Button with thirty boolean props by favoring composition over configuration. Split presentational from container components, set clear prop boundaries, and make design tokens the single source of truth so the codebase scales instead of rotting.
01The Button with thirty boolean props
Open the design-system Slack of any growing company and you will eventually find this message: "hey, does anyone know what isCompactSecondaryGhost does?" The Button started life as a humble little thing. Then someone needed a loading state, so they added isLoading. Then an icon, so hasIcon and iconPosition. Then a destructive variant, a ghost variant, a compact size, a full-width mode, a tooltip-on-disabled hack. Two years later the type signature is a wall.
Nothing about that compiles wrong. <Button isPrimary isSecondary isGhost /> typechecks perfectly and renders nonsense. The component has become a configuration language nobody designed on purpose. This article is about the discipline that prevents it: composition over configuration, clear component boundaries, prop design that closes invalid states, design tokens, and how all of it rolls up into a reusable component library, a real design system instead of a junk drawer.
Who this is for
You ship features in a React/Vue/Svelte codebase and you have felt the rot, components that fork into copies, props that contradict each other, "don't touch that file" comments. You want the senior-level mental models for keeping a UI codebase healthy as the team and surface area grow. Some component framework experience assumed; the principles are framework-agnostic.
02The one principle: compose small pieces
Compose small pieces; don't configure giant ones. Every boolean prop you add is a branch inside the component, every slot you expose is a branch you handed to the caller.
A configurable component tries to anticipate every use case and exposes a prop for each one. The component owns all the variation, so it grows without bound. A composable component does one small job well and lets callers assemble it with siblings and children to get variation. The complexity moves out to the call site, where it is visible and local, exactly where the person who needs the variation is already standing.
A single molded plastic toy, one fixed shape, no partsA giant component with 30 props: every new need means re-molding the whole thing
A box of LEGO bricks, small, uniform, snap togetherSmall primitives (Box, Text, Stack, Button) you compose into anything
Want a castle and a spaceship from the same bricksSame primitives compose into a form, a card, a toolbar, no new code in the primitive
A brick has studs in standard positions so any two fitProps and tokens are the standard interface that lets pieces snap together predictably
Why composition scales and configuration doesn't.
LEGO won because the *interface* between bricks is small and uniform. That is the entire trick. Your primitives should have small, uniform interfaces too, then features are assembled, not authored from scratch each time.
03The picture: how a UI codebase layers
A healthy frontend isn't a flat pile of components, it's a layered dependency graph. Lower layers know nothing about higher ones. Tokens don't know about buttons; buttons don't know about the checkout page. Dependencies point one way, down.
Design tokens flow up into primitives, which compose into reusable components, which compose into features and pages. Each layer depends only on the layer below.
1
Define tokens
Name your raw decisions once: color.brand.500, space.4, radius.md, font.size.lg. No component hardcodes a hex or a pixel value, they reference tokens.
2
Build primitives on tokens
A Box maps props to token-backed styles. A Stack lays out children with a token-based gap. Primitives are the only layer that touches raw CSS.
3
Compose reusable components
Button, Input, Card are built from primitives. They encode brand decisions (a Button always uses brand color + md radius) but stay generic about content.
4
Assemble features
LoginForm composes Input + Button + Stack. It knows about your domain; the components below it do not.
5
Compose pages
Routes wire features together. By the time you reach a page, you are arranging blocks, not writing CSS or fixing a Button.
6
Theme by swapping tokens
Dark mode and white-label brands change token *values*, not component code. The whole tree re-themes for free because nothing below references a raw value.
04Presentational vs container components
Within the component layer there's a second split worth keeping in your head, even in a hooks-everywhere world. Presentational components take data and callbacks via props and render UI, no data fetching, no global state, no business rules. Container (or "connected") components know *where* data comes from, they call hooks, read stores, fire mutations, and hand plain props down to presentational children.
The payoff is testability and reuse. A presentational UserCard({ name, avatarUrl, onFollow }) renders identically in Storybook, in a test, and in three different features, because it has no idea where name came from. The container ConnectedUserCard does the useUser(id) plumbing and renders <UserCard {...user} onFollow={follow} />. Keep the wiring thin and shove all the actual UI into the dumb component. Modern code rarely needs two literal files for this, but the *boundary*, "this component fetches, that one only renders", is what keeps things sane.
Pro tip
Rule of thumb: if a component both calls useQuery/useStore *and* contains real layout/markup, it's doing two jobs. Split the data-fetching shell from the presentational core. The core becomes trivially reusable and your design system can ship it.
05Composition vs configuration: the trade-offs
Configuration isn't *always* wrong, a variant="primary" enum prop is fine and good. The danger is when configuration is used to bolt on *structural* variation that should have been composition. Here's how to tell them apart by their symptoms.
Dimension
Configuration (prop-driven)
Composition (slot/children-driven)
Adding a use case
Add another prop + an internal branch
Caller arranges existing pieces, no edit to the component
Symptom of overuse
Prop explosion; mutually exclusive booleans
Deep nesting at call sites if primitives are too granular
Every design tweak edits this file and adds a branch. Now the composable version: Card owns *only* the box (background, radius, shadow, padding, all from tokens) and exposes slots via children and sub-components.
Notice what happened. Card will never grow another prop for a new layout. A wishlist card, a comparison card, an empty-state card, all of them reuse the same Card and arrange different children. And Button keeps a single variant enum instead of ten booleans, because enums make invalid states unrepresentable: you literally cannot pass variant="primary" variant="ghost". That is prop design as a safety rail, model the finite axes as enums, the open-ended structure as children.
Watch out
Prop boundary rule: a component's props are its public API. Once teams depend on a prop, removing it is a breaking change. Add props reluctantly, name them for *intent* (tone="danger") not implementation (color="#e11"), and prefer children over a content prop whenever the slot might hold arbitrary markup.
07Design tokens: the source of truth
A design token is a named design decision stored as data, not baked into code. Instead of #d97706 scattered across forty files, you have color.brand.500 defined once. Tokens are what let a design system stay consistent and re-theme without a rewrite. They usually come in two tiers: primitive tokens (the raw palette, purple.500 = #d97706) and semantic tokens (the meaning, color.action.primary = purple.500). Components reference *semantic* tokens, so you can repoint color.action.primary to a new shade and every button updates.
tokens.ts, semantic on top of primitive
tsx
// Primitive tier: raw values, no meaning yet.exportconst palette = {
purple500: "#d97706",
pink500: "#ec4899",
gray900: "#111827",
gray50: "#f9fafb",
} asconst;
// Semantic tier: meaning, mapped to primitives. Components use THESE.exportconst tokens = {
colorActionPrimary: palette.purple500,
colorTextDefault: palette.gray900,
colorSurface: palette.gray50,
spaceSm: "8px",
spaceMd: "16px",
radiusMd: "8px",
} asconst;
// Dark mode = a different semantic mapping, same primitives + components.exportconst darkTokens = {
...tokens,
colorTextDefault: palette.gray50,
colorSurface: palette.gray900,
} asconst;
In practice these are emitted as CSS custom properties (--color-action-primary) so theme switching is a single class on <html> and costs nothing at runtime. Tools like Style Dictionary turn one token source into CSS, JS, iOS, and Android outputs, one decision, every platform. The point isn't the tooling; it's the discipline: no raw values below the token layer.
08From component library to design system
People use "component library" and "design system" interchangeably, but they aren't the same. A component library is the code, the reusable Button, Card, Input. A design system is that library *plus* the tokens, the usage guidelines, the accessibility contract, the documentation, and the shared vocabulary between design and engineering. The library is the artifact; the system is the agreement.
Tokens, the single source of design truth (covered above).
Primitives + components, composable, token-backed, accessible by default (focus rings, ARIA roles, keyboard handling baked in so feature teams can't forget).
Documentation, live examples (Storybook), prop tables, and "do / don't" guidance. Undocumented components get re-invented.
Contribution model, how a new component gets proposed, reviewed, and promoted from a feature into the shared library. Without this, everyone forks.
Versioning, semver on the package so breaking a prop is a deliberate, communicated event, not a surprise on Monday.
Start smaller than you think. You do not need 80 components on day one. Ship tokens + five primitives + the four or five components every feature actually uses (Button, Input, Card, Modal, Stack). Let real usage pull the next component into existence, promote it *after* it has been copy-pasted twice, not before.
09Common mistakes that rot a codebase
1Prop explosion. Bolting structural variation onto a component as booleans until it has 30 props and four mutually exclusive ones. Fix: reach for children/slots and the compound-component pattern before adding a prop.
2Premature abstraction. Building a "flexible" generic component for a use case you have exactly once, guessing at the future API. The rule of three: don't extract a shared component until the third real usage shows you the actual shape. Wrong abstractions cost more than duplication.
3No tokens. Hardcoded hex codes and pixel values sprinkled everywhere. Dark mode becomes a multi-week migration and brand consistency is a manual code review. Fix: a token layer from day one, even a tiny one.
4Copy-paste components. "I'll just duplicate UserCard into AdminCard and tweak it." Now a bug fix has to be applied in two (then five) places and they drift. Fix: compose the shared core, vary via props/children, and a contribution model so the shared version is the easy path.
5Mixing data and presentation. Components that fetch *and* render can't be reused or tested in isolation. Fix: keep the presentational core dumb and push the wiring to a thin container.
10Takeaways
The whole article in nine lines
Compose small pieces; don't configure giant ones, every boolean prop is a branch you own forever.
LEGO beats a molded toy: small, uniform interfaces let you build anything without re-molding.
Layer the codebase: tokens → primitives → components → features → pages, dependencies pointing down only.
Split presentational (renders props) from container (knows where data lives), the dumb core is the reusable one.
Use enums for finite axes (size, variant) and children/slots for open-ended structure; that makes invalid states unrepresentable.
Treat props as a public API: name for intent, add reluctantly, prefer children over a content prop.
Design tokens are named decisions as data, primitive tier for raw values, semantic tier for meaning; no raw values below the token layer.
A component library is the code; a design system is library + tokens + docs + contribution model + versioning.
Start small and let real usage (the rule of three) pull abstractions into existence, never the other way around.
11Where to go next
Architecture is one of three pillars of a healthy frontend. Once your components compose cleanly, the next questions are where state lives and how the visual layer is built underneath your tokens.
Continue the track: the Frontend Engineer path sequences these senior topics with the fundamentals beneath them.
Practice: pick your ugliest god-component and refactor it into a compound component this week. Pull the variation out to the call site and watch the prop list collapse.
A component is accumulating contradictory props and you need to add another variation. What do you do?
Check your understanding
1. What is the central principle the article offers for keeping a component healthy as it grows?
2. How does the article describe the layering of a healthy UI codebase?
Frequently asked questions
What does composition over configuration mean for components?
A configurable component tries to anticipate every use case with a prop for each one, so it grows without bound and owns all the variation. A composable component does one small job well and lets callers assemble it with siblings and children, moving the complexity out to the call site where it is visible and local.
Why are too many boolean props a problem?
Every boolean prop you add is another branch inside the component, and combinations like isPrimary, isSecondary, and isGhost can typecheck perfectly while rendering nonsense. The component slowly becomes a configuration language nobody designed on purpose, with a type signature that turns into a wall.
What is the difference between presentational and container components?
Presentational components take data and callbacks via props and render UI, with no data fetching. Container components handle the data side and pass it down, keeping concerns separated even in a hooks-everywhere codebase.
What are design tokens and why do they matter?
Design tokens are the single source of truth for design values, sitting at the lowest layer of the dependency graph where they know nothing about buttons or pages. Dependencies point one way, down, so tokens feed components and components feed features, which keeps the codebase from rotting as it grows.
Was this article helpful?
Want to go deeper?
This article covers concepts taught hands-on in the Cloud Engineer and DevOps career paths, with real terminal labs, production scenarios, and structured lessons.