Back PJ Onori’s blog

The many layers of dependencies in design systems

Building a design system from absolute scratch is not a small feat. Modern interfaces are hilariously complex. “Hello world” in a modern app requires a lot of code and expertise. This almost always means that building on top of someone else’s code/technology. I’m calling this layering. Yes, there are more technical terms, but I’m a simple guy who like’s small words. I also want to avoid technical terms because layering isn’t just about code. This post focuses on web design systems to keep a consistent train of thought. But in my experience, the thinking applies no matter the platform.

There’s no such thing as an unlayered design system

A design system is always using something to render a UI component. It can be as “low-level” as layered on vanilla HTML/CSS/JS. It can be layered on a framework like React. But it doesn’t stop there. Design systems often layer on top of headless component libraries like React Aria. They layer on full design systems, like many have done with Material. It can even add layers on top of that–like adding data-viz components to Material.

All design systems are layered on top of something. the million dollar question is how high (or low) should you go? Any why? But keep in mind, the higher up those layers go, the more things become interesting.

Making a design system on top of Bootstrap

One of the first design systems I worked on, circa -6000 BC, was built on top of Bootstrap. That had a lot of advantages. It made initial progress incredibly fast. It was the most used framework around at the time. It was familiar and reduced the learning curve for those using it. And given how many decisions Bootstrap took care of, we had much less initial load to carry. It made a ton of sense and worked well…

There’s always the, ‘…’

Initial progress was quite fast–as long as we played nicely with Bootstrap. Things got tricky when we tried to bend it to our will. The wheels began to wobble once we began including major CSS overrides. It only got harder from there. And managing Bootstrap updates… That was “fun”. As Bootstrap evolved and changed, our “fixes” would break. This made the codebase eventually feel like a vat of pasta.

Was this the fault of Bootstrap? No. We wanted the framework’s benefits without the costs of layering on a framework. That’s not how it works.

The pros are the cons (and vice versa)

What I’ve come to believe is that every upside of layering is also its downside. The trick is to maximize the upsides’ benefits while slimming the downsides.

A lot of the decisions have been made

Building a design system from scratch requires making an eye-watering amount of decisions. Layering on top of an existing framework makes a lot of those decisions. Fighting those decisions can make life very hard. It’s important to know what you’re getting into–and what decisions you want to control.

If flows and patterns are the priority, layering right on top of Material could make sense. Want full control over how the system looks? That means a headless component library like React Aria is probably the ceiling. Want more control over what is rendered in the DOM (and how)? Go even lower. Every layer descended means more control. It also means more decisions to make. Oh, and more work to do. Rule of thumb: Less layers means more control, but more headaches (from more work). More layers means less control, but more headaches (from managing/supplementing dependencies).

Either way, it’s important to know you don’t just inherit the decisions of what you’re layered on. You also inherit the decisions from the things they’re layered on. Engineers understand this, designers may not. Run npm install @carbon/react and see how much more comes along in node_modules.

And that’s no shade on Carbon. But it’s important to know. Dependencies have dependencies–that you now depend on.

Building on top of an established codebase

The value of layering on top of established libraries is that they’re established. A lot of the issues, edge cases, and bugs are already ironed out. Those would all be work to plow through–which is daunting. Sure, LLMs can pump out code pretty fast. But time is typically a big gating factor to get things working well.

But, established can also come with baggage. The codebase may have accumulated tech debt. Or it’s built with stale ideas/technologies. Or it contains a yet-to-be-discovered security hole. Yes, you get the good of established functionality and bake-time. But you also get the bad that comes along with time.

Tap into an shared mental model

Layering on top of a library like React can be a quick win. People know how to use it–they can rely on muscle memory. Everyone can hit the ground running. This is no small part why React so pervasive with design systems. It’s critical for design systems to meet people where they are. This is not something to ignore.

Tapping into an established library is a tacit endorsement of their point of view. Ignore or buck that point of view and risk losing that shared mental model. What about those who don’t agree with that point of view? Let’s say a team doesn’t buy into “the React way”. They disregard those best practices when building their design system components. They could be leaving a lot of value on the table if people aren’t able to use the React components like React. So be careful how much you buck what you layer on–you may be missing the point.

On the other hand, what’s hot today is gone tomorrow. Who uses Bootstrap anymore (rhetorical question)? The asset of leveraging a popular library could someday become a liability. Teams that disagreed but followed suit anyways may be feeling regret.

Community-backed

Using an open-source, community backed library means not being alone. There’s support from other people contributing, pressure testing, and answering questions. This feels like a life-line for small teams with limited resources.

But when being a part of a community, that means being a part of a community. Meaning being one voice of many. People may not be interested in your requests. Your contributions may not be accepted. The roadmap may not be headed in a direction you don’t like.

Or, the community dries up and development stops altogether. Now you depend on a codebase created by other people, but you need to support. On your own.

Continuous improvements

Many libraries are constantly updated and improved. A lot of that work would be things your team wouldn’t have time for. This frees up to work on areas are worth the time and focus.

But update fatigue is a real thing. And not all those “improvements” may seem like improvements to you. Some of the changes may not align with your point of view. There will be breaking changes that involve migrations. Some migrations can be incredibly time intensive. Sure, you can stick to a previous version, but that comes with its own set of risks…

There’s no right or wrong here

Like I said, every rose has its thorn. The “right” decision has a lot to do with what you’re aiming to do–and why. Everyone is going to have their own mental questionnaire. Below is mine:

How many frameworks need supporting?

A lot of systems don’t have the luxury of supporting a single framework. It’s critical to focus on the common denominator when supporting more than one. Need your components to be available in React and Vue? The last thing you want is to have to update styles in two places. So, it’ll going to be important to have a one solid HTML/CSS structure that both pull from.

How much expertise (and time) is really available?

It’s tempting to want full control over a system. But are you prepared to build everything from the ground up? That may sound fun, but you have to ask yourself if have the time and chops to pull that off. React isn’t perfect, but it’s one hell of an accomplishment. Same with Material. It’s important to be honest about what you’re ready to take on and live with. And not just about you. Are you sure you can make the thing that supports hundreds of designers and engineers on a daily basis? To power a product millions use? Think about that before you sign on the dotted line.

How loose can the dependency be?

Not all layers need to be tightly coupled. Sometimes a little more upfront work can save a ton of headache in the future. For instance, I recommend managing design tokens in JSON instead of Figma. JSON is a standard, portable format and can go anywhere. Yeah, that means those JSON values will need to be imported and synced. But it also represents one less hard dependency in case a design tool migration needs to happen.

Being able to “rip the cord” is an immensely liberating feeling.

How specific are the needs?

Does the business rely on supporting an old legacy browser? What happens if/when the UI framework drops support? How much risk does that represent?

What’s Plan B?

It’s worth taking a moment to consider how to avoid a worst case scenario. Try to pre-plan a graceful exit where a dropped dependency doesn’t spell doom. For me, that means biasing towards standard formats and technologies. Sure, some folks prefer the developer experience of CSS-in-JS. But CSS-in-CSS can be used in any web stack.

Who are the people working on those dependencies?

How long have they been around? Do they have a history of doing good work? Are they trusted? The design system is now partially relying on these people. It’s worth knowing who they are.

How much functionality is actually needed?

Don’t inherit the Titanic to ship a canoe. Keep a design system as small as possible. It may turn out 99% of what a library offer isn’t relevant. It’s probably worth building that 1%.

What’s the endgame?

If the goal is to spin up a product and sell it in a year or so, layer like it’s going out of style. A lot of the downsides of dependencies only show up over time. The longer the horizon, the more beneficial ownership and control becomes.

Own what matters

No design system is built completely from scratch. That’s impossible. The moral of this story is not to hand crank everything. It’s to be intentional about what you own and what you don’t. A system focused on strong presentation better damn well own its visual styles. If it’s first class developer experience, then again, you need to own it. Dependencies aren’t to be avoided at all cost. Neither are they something to take on willy-nilly. They’re a necessity–both a pro and a con–to consider with care.