A design system allows you to create a modern web app with consistent appearance and behavior without debate or iteration. And components are the perfect building blocks for your design system.
How do you build a “modern” web application with consistent appearance and behavior while avoiding endless debates and iterations to figure out which colors to use, how big your headings should be or how controls like buttons should behave?
If you’ve ever spent more than a few minutes hunting down an elusive line of CSS just to make a heading “look right,” you know how much time this can eat up, and how quickly it can spiral into hours of tinkering, getting feedback and making tweaks until everyone’s happy.
Thankfully, it doesn’t have to be this way. What you need is a system!
You may have heard of “design systems.”
A design system is an attempt to codify various aspects of your web application’s design in such a way that designers, developers and anyone else involved in creating the app can work together to deliver an app which is both visually and behaviorally consistent.
At one level this means agreeing on things like typography, colors and logos.
But it goes deeper than that, with consideration also given to other aspects of your app, like how interactions (gestures, animations, sounds, etc.) should work.
You can think of a design system as a toolkit for your app. With everyone’s input, you can arrive at an agreement for many aspects of your application’s UI, freeing you up to focus on your code (and adding new features).
It’s tempting to think that this is primarily a tool for designers or other stakeholders for whom the appearance and behavior of the app is paramount.
But as a developer who needs to make all of this work, the design system is a real opportunity to create building blocks for your application that make it easier to keep the UI in line with everyone’s expectations (and consistent), while also rapidly creating new parts of the UI by composing different combinations of these building blocks together.
There are many ways to think about the components which form a web application, but one I find particularly useful is Brad Frost’s Atomic Design Methodology.
He uses a chemistry analogy to think about how to break a UI design down into smaller pieces:
These are the basic building blocks of the web—or, as we know them, HTML elements.
At this level we’re talking about things like forms, labels, inputs, etc.
Brad describes molecules as a combination of atoms.
So if you were to combine a few labels and inputs into a form, the form would be an example of a molecule.
This is where we group molecules together to form relatively complex, distinct sections of the user interface.
It’s here that you might create a banner, or a navigation menu, or a list of products for your application.
The key is that we get to take our molecules and compose them together to form a more concrete part of the UI.
The good news is this approach ties in very nicely with modern component-based frameworks (such as React, Angular, Vue, Blazor and others).
Once we realize that HTML elements are essentially atoms, the next job is to create some reusable molecules.
It might help at this point to think of a concrete example.
<h1>Hello World</h1>
Probably the lowest hanging fruit when coming up with a design system is typography.
In order to create a design system for your app, there would be some form of collaboration between team members in order to arrive at key decisions about the appearance and behavior of core building blocks (such as headings and paragraphs).
Once those decisions are made, the next step is to represent these appearance/behavior decisions in code.
At this point you, as the developer charged with building this system, have a few options.
First of all, and especially for things like headings and paragraph fonts, you can define basic typography styles in a stylesheet (which can then be used across the entire application).
h1 {
font-size: 1.2em;
}
h2 {
font-size: 1.1em;
}
This remains a convenient (and performant) way to take care of quickly applying base styles to your app.
But while CSS is good for controlling the appearance of your UI elements (atoms), what if you want to create something a little more complex?
Let’s say you’re working on a site which displays technical documentation and the design system indicates that technical terms should be highlighted to make them stand out.
One option would be to just use <code>
tags directly:
and this is where we can make use of <code>window.localStorage</code> to store our users preferences...
You could then use CSS to style all code
tags to make these leap off the page.
But this use of the raw code
tag suffers a few limitations.
It doesn’t provide any extra context around what this actually represents (a technical term), nor does it make it very easy to implement custom behavior across the entire application.
Let’s say the design system for this site indicates that users should be able to hover over technical terms to see a brief definition of them.
If we just use <code>
tags everywhere, it’s challenging to implement that hover-over behavior consistently across the site, but things get significantly easier when we create our own custom components.
While we’re at it, we can give our components more specific names to match the terminology used in the design system.
and this is where we can make use of <term definition="Part of the Web Storage API which enables storage of key/value pairs">window.localStorage</term> to store our user's preferences...
Now we have a handy reusable component (molecule) which will look the same, and behave consistently, across the entire application, and is clear about what it represents.
What’s more, if you found yourself talking about window.localStorage
more than once, you could create a specific component just for this term, which wraps the underlying term
component…
TermLocalStorage Component
<term definition="Part of the Web Storage API which enables storage of key/value pairs">window.localStorage</term>
Then use it in multiple places…
and this is where we can make use of <termLocalStorage /> to store our user's preferences...
<termLocalStorage /> is a really handy way of storing user preferences which aren't permanent enough to store in a backend DB...
It’s still easy to tweak the underlying term
component if the design system changes, but you avoid duplicating the same definitions over and over again throughout the site.
A key part of the design system lies in controlling the appearance of specific elements.
When building with components, you have a few options to handle this:
The first option is one we’re probably all familiar with, to create stylesheets and let those do all the heavy lifting.
The nature of CSS when you go with this approach is you either end up with styles that affect lots of elements in your app, or lots of id/class selectors to apply more specific styles to individual elements (or groups of elements).
h1 {
/* styles for all h1 elements */
}
#product-listing .details {
/* styles for the any elements with the .details class inside a product listing */
}
However, when you’re working with components, you often want your styles to apply to specific components, and avoid those styles bleeding out to other parts of your app.
While this can be achieved with id and class selectors in stylesheets, that method often spirals into a large amount of “co-located” CSS, which in turn often leads to unexpected results, with elements picking up styles that were never intended for them.
The alternative is to isolate (or scope) your styles to one specific component.
As it turns out, there are a few ways to achieve this “CSS Isolation.”
Tailwind CSS has gained traction in recent times as a convenient way to control the style of specific components (but without resorting to inline styles). (Read another Telerik writer’s recommendations when using Tailwind CSS here.)
You can use Tailwind’s utility classes directly in your markup to control the style of individual elements.
Here’s how that might look for our term
component.
Term Component
<code class="px-2 bg-blue-400 inline-block text-gray-50">
Term goes here (passed in as a parameter/prop)
</code>
Which renders this in the browser:
Because this is neatly encapsulated in a single reusable component, you don’t need to worry about this specific markup being repeated multiple times throughout the app (as you’ll simply keep reusing the same term
component instead).
Tailwind provides a quick way of iterating your UI until it matches the design system you’re trying to implement.
With hot-reloading (as now offered by most of the web frameworks), it’s speedy to keep tweaking these styles until you end up with something you’re happy with.
It is a little jarring at first to see so many classes in your markup. But if you go down this route, you quickly find that the real benefit comes in how easy it is to iterate your UI.
Tailwind’s utility classes are consistently named so you quickly arrive at a point where you can (usually successfully) intuit which classes you need.
For example, p-
controls padding, px-
controls horizontal padding and py-
controls vertical padding. From there it’s logical that pl-
controls left padding, and so on.
The other options for creating component-specific styles vary depending on which framework you’re using.
In all cases you can define styles for a component (for example, our term
component) which affect only that component. This leaves you free to define styles for basic HTML elements within those components (like headings, paragraphs, etc.) without any danger of those styles “escaping” and affecting other components/parts of your UI.
Here’s an example using Blazor’s CSS isolation.
Term.razor.css
code {
background-color: #737CA1;
padding-left: 0.5rem;
padding-right: 0.5rem;
color: white;
}
Term.razor
<code>
@ChildContent
</code>
@code {
[Parameter]
public RenderFragment ChildContent { get;set; }
}
In this example, the style we’ve defined for code
will only affect usages of code
within the Term
component. If we were to declare a code
element somewhere else in the app, the style(s) from Term.razor.css would not apply.
Although they vary slightly in implementation, all the frameworks have broadly similar techniques for defining styles specific to a component in practice.
Continuing with the chemistry analogy, it’s worth noting there’s no real limit to how many of these building blocks (atoms and molecules) you might end up with.
Take a look at the controls in a comprehensive design system such as Microsoft’s Fluent UI, and you’ll see you can go a long way defining these reusable components and using them as the backbone of your user interface.
Toggles, sliders, date pickers, breadcrumbs, ‘Like’ buttons, emoji pickers… Whatever you need for your app, once it’s in the design system and you’ve got the component for it, you’re free to use it wherever and whenever you like to build the rest of your UI.
As you continue down this path and end up with your core collection of reusable components, these components will (hopefully) have very clear and consistent styles and behavior (adhering to the design system).
But what about building more complicated parts of the UI?
Well the good news is you already have the building blocks—now it’s a case of putting them together.
Say you’re building an online store and you need to show a list of products.
You may well have already created some of the components you need (headings, images, buttons). Now you get to the fun part where you can compose those together to make a larger, slightly more complex, distinct section of your user interface.
Brad Frost calls these more complex UI parts organisms.
This is the point where you’re likely to find yourself moving from reusable general components to more specific parts of the UI.
Even in this simple example we begin to see the benefit of creating those building blocks in the first place.
Take the price for example. Once you’ve got a price
component, you can use that everywhere and be sure that the prices will always match the design system.
<price amount='121.12'/>
If the design system is subsequently updated to allow for discounted prices to be displayed alongside the original price (with a strike through the original price), you can extend this component to handle discounts and display them accordingly.
<price amount='121.12' offerPrice='89.90' />
If all images are required to have alternative text, you can make this mandatory for your image
component—then these product images (along with every other image in your app) will need require alt text.
If all buttons should show a certain color when clicked, you can define that in your primaryButton
component and then forget about it (until the design system changes!).
When you go down this road, you may be surprised just how little UI work you have to do to get lots of very different parts of your UI in place, because you can use components you’ve already built (or quickly create new ones).
To make sure your components are truly reusable (as in the examples we’ve seen here) and therefore solid bedrocks for your design system, it pays to be mindful about where you put your logic.
In our example, any logic specific to the product list should probably go into the Product List component (or organism), which is composed of the other reusable components we’ve explored. Because this level of component is (naturally) more concrete/specific in terms of your app, it makes sense that it should include logic for this specific use case.
But beware accidentally pushing context-specific logic down into your components if you’re hoping to reuse them elsewhere.
If you find yourself putting a lot of logic, especially conditional code, into your lower-level components (atoms or molecules), you may well be making them either too specific to one use case, or too generic in that they’re trying to handle too many cases at once.
If this happens, there are two possible problems (and solutions).
Firstly, conditional code might be telling you you need another, more specific component.
Take our humble headings for example.
If you have one heading component with lots of code to handle, whether it’s a top- or second-level heading (heading 1 or heading 2), your component might be doing too much.
If so, consider creating two components, perhaps a primaryHeading
and secondaryHeading
, each with their own specific logic.
The other possibility is that your component has become too specific to one use case.
To take a slightly silly example (but still, quite easily done), let’s say you modified your PrimaryHeading
component to prefix the heading with the word “Product” because you needed that for one part of your app.
You might accidentally fall into the trap of putting conditional code into the PrimaryHeading
component to try and show/hide the “Product” prefix depending on some other context (for example, a flag passed to the component).
<PrimaryHeading isProduct={true}>An Amazing Product</PrimaryHeading>
In reality, the “Product” prefix is context-specific and so probably shouldn’t be in the PrimaryHeading
component at all, but controlled in a component higher up the component tree.
<PrimaryHeading>Product - An Amazing Product</PrimaryHeading>
Now PrimaryHeading
remains oblivious to the nuances of whether a heading should have a “Product” prefix or not!
In general, a good rule of thumb is to keep your components focused on doing one thing well. If they start doing too much, either create a new component, or pull logic up the component tree.
Design systems are valuable for everyone involved in designing (and implementing) a web application.
The component-based approach to building modern web applications makes for an effective solution to implementing a design system using a combination of:
With this approach, you can focus on building key parts of the UI using the components you’ve already built, safe in the knowledge that, as the design system evolves, so can your components.
Jon spends his days building applications using Microsoft technologies (plus, whisper it quietly, a little bit of JavaScript) and his spare time helping developers level up their skills and knowledge via his blog, courses and books. He's especially passionate about enabling developers to build better web applications by mastering the tools available to them. Follow him on Twitter here.