2025-11-05
Building components for scale
4 min read - Benoit Rajalu
The core of a design system is its component library. Its role is to provide trusted components to accelerate UI building. This capacity however relies on your components architecture: in order to be trusted, they have to be able to scale.
The original promise
A design system enforces scalability: you invest in it because it lets you consolidate your highly-reusable UI elements in a single source.
Those components have to be trustworthy. They must be bug-free (obviously). They must achieve the expected looks and behaviors each time, for every consumer, on every feature. Without this, your component library creates friction and its users will eventually find ways to bypass it.
But you also need to care for one last promise: your components must be built to scale, and that scalability has to come in two flavours. The first one is easy: updating the components must update them everywhere they are being used. The second one is not so simple: components must easily be spun into new variants, and variants must be able to grow together.
This comes from the same place as the quality requirements: without this scalability, your design system will introduce friction on change, which is always certain to push consumers and sponsors away. Don't be an obstacle if you want to stay relevant.
Holistic architecture
Before we discuss how we can do this, let's focus on one more criteria: components have to be scalable for every platform the design system serves. Which means your design source just as much as your source code.
Design systems are a shared language between designers and engineers. Following the same building principles helps ensure that connection. If your Figma architecture enables things your engineering one doesn't (and vice versa) then something will get lost in translation. That's an issue for human-based workflows, but it is one just as well in the current AI environment. Figma's MCP server is much more efficient when code and design sources are highly similar, and any automated design-to-code process can only benefit from structural parity.
Atomic design
Isn't this just another way to push for design system best practices and for the (now classic) atomic design architecture pattern? Yes. Yes it is.
But in a way that we discuss much less. Atomic design usually focuses on building your atoms (buttons, icons, inputs, typographic elements etc...) and then graduating to the molecules (combining atoms) and organisms (combining molecules) up to the entire screens your business needs.
This is about looking at how you build even atoms, and how different a public atom can be from a private one.
Design architecture
I'm going to assume we're working with Figma, but the concept applies everywhere.
Let's consider a family of generic components. They need to handle generic content, but also affixes. Then comes a variant with a different kind of suffix. Then another one that needs to be clickable. How do we build those in a way that scales? We break it down into parts.
We identify the common, reusable parts (a wrapper, a main content, our prefix, our suffixes...) and make primitives of those. These will be our private ingredients. Once we have them, what's left is assembling them to "bake" the public component variants we need, with their differences and their common parts.

We've ensured the primitives won't get out of the library (with the .Name formatting), but we've built our actual public components using them. Now if we need to change those together, we can: we just update the primitives. And if we need to update them separately we also can, by creating differing primitives as needed!
Code architecture
This pattern is easy to replicate as code and provides the same benefit. We can build the same primitive components, private to a package and thus not reached by the library's consumers.
Here's a very generic example. Given a structure where only this index.tsx file will be exposed to consumers, we can build it using a series of private atoms:
// Line/index.tsx
import * as Content from "./primitives/Content.tsx";
import * as Prefix from "./primitives/Prefix.tsx";
import * as Suffix from "./primitives/Suffix.tsx";
import { Wrapper } from "./primitives/Wrapper.tsx";
export const Line = ({
prefix,
content,
suffix,
}: {
prefix?: Prefix.PropTypes;
content: Content.PropTypes;
suffix?: Suffix.PropTypes;
}) => {
return (
<Wrapper>
{prefix && <Prefix.Prefix {...prefix} />}
<Content.Content {...content} />
{suffix && <Suffix.Suffix {...suffix} />}
</Wrapper>
);
};
The pattern is powerful for building variants, making component families with variations and specific rules but shared elements.
// Line/variants/default.tsx
import * as Content from "./primitives/Content.tsx";
import * as Prefix from "./primitives/Prefix.tsx";
import * as Suffix from "./primitives/Suffix.tsx";
import { Wrapper } from "./primitives/Wrapper.tsx";
export const Default = ({
prefix,
content,
suffix,
}: {
prefix?: Prefix.PropTypes;
content: Content.PropTypes;
suffix?: Suffix.PropTypes;
}) => {
return (
<Wrapper>
{prefix && <Prefix.Prefix {...prefix} />}
<Content.Content {...content} />
{suffix && <Suffix.Suffix {...suffix} />}
</Wrapper>
);
};
// Line/variants/data.tsx
import * as Content from "./primitives/Content.tsx";
import * as Prefix from "./primitives/Prefix.tsx";
import * as DataAffix from "./primitives/Data.tsx";
import { Wrapper } from "./primitives/Wrapper.tsx";
export const Data = ({
prefix,
content,
data,
}: {
prefix?: Prefix.PropTypes;
content: Content.PropTypes;
data?: DataAffix.PropTypes;
}) => {
return (
<Wrapper>
{prefix && <Prefix.Prefix {...prefix} />}
<Content.Content {...content} />
{data && <DataAffix.Data {...data} />}
</Wrapper>
);
};
// Line/variants/clickable.tsx
import * as Content from "./primitives/Content.tsx";
import * as Prefix from "./primitives/Prefix.tsx";
import * as Suffix from "./primitives/Suffix.tsx";
import { WrapperClickable } from "./primitives/WrapperClickable.tsx";
export const Clickable = ({
wrapper,
prefix,
content,
suffix,
}: {
wrapper?: WrapperClickable.PropTypes;
prefix?: Prefix.PropTypes;
content: Content.PropTypes;
suffix?: Suffix.PropTypes;
}) => {
return (
<WrapperClickable {...wrapper}>
{prefix && <Prefix.Prefix {...prefix} />}
<Content.Content {...content} />
{suffix && <Suffix.Suffix {...suffix} />}
</WrapperClickable>
);
};
Now any enhancement to a primitive will benefit all the variants. Building a new variant is just as easy while keeping debt at a manageable level.
Conclusion
The core principle behind atomic design is composition, but we often only consider it when we build features. The point here is to consider it also when we build components.
And it starts with decomposing. Look at what you need to build, and split it down to its reusable parts. Make those primitives, the private building blocks of your components. With this strategy, your components can scale both within their roles in the product and through new, cheap and sustainable variants. You've built them for scale!