Resilient CSS APIs and Design Systems
Cory Rylan
- 3 minutes
Design Systems and UI libraries help companies and product teams align visual design consistency and speed up UI development. As much as we hope to make our UIs pixel perfect, many aspects of the Web actively work against pixel perfection. Not only do we have to deal with browser inconsistencies but also other code running in the application unintentionally breaking or changing our expected visual output.
Many Design Systems and UI libraries use global CSS to apply the website's desired look and feel. Global CSS can be good and bad in different use cases. Global styles can help easily make things consistent but also cause conflicts. Let's walk through a seemingly simple scenario.
Our Design System needs consistent styles for our h1
headings. We define something like this in our code:
h1 {
color: #2d2d2d;
font-size: 24px;
}
Great, now every h1
heading will look the same. Quickly we run into issues at scale. What if we want to use an h1
style but can't due to an h1
existing already on the page? Duplicate headings are bad for SEO and accessibility. Well, we could make an h1
class.
h1, .h1 {
color: #2d2d2d;
font-size: 24px;
}
This additional selector is a step in the right direction. We can now use any semantically correct element to style it like our system's h1
style. But this is still at risk of other issues. First, if we are building a Design System to support multiple apps or teams, they may be already using another library to style their app. By applying styles to native element selectors, we have a high risk of causing style collisions.
.h1 {
color: #2d2d2d;
font-size: 24px;
}
So if we remove the native element selector, now we eliminate the risk of the Design System colliding with other styles. This now leaves the question of why call it an h1
? Why not name it to something semantic that makes it easy to understand its purpose?
.heading {
color: #2d2d2d;
font-size: 24px;
}
That's better, but the class name is a bit generic and still could be more specific. We can prefix with some standard name for our system.
.ds-heading {
color: #2d2d2d;
font-size: 24px;
}
The prefix could be a shorthand name for the product or company. Here I have ds
for Design System. So now, we have a scoped isolated and consistent way to style headings in our design system. Something as simple as a class name can be more complicated than it seems. Thoughtful API design can save teams significant amounts of time when trying to adopt, use, or upgrade to your Design System/UI Library.
Often I think CSS selector types are often not used to their potential. While CSS class names are the most common style hook, we can use HTML attributes to provide a more expressive CSS API.
[ds-text*='heading'] {
...
}
[ds-text*='subheading'] {
...
}
[ds-text*='body'] {
...
}
<h2 ds-text="heading">A cool heading</h2>
<h2 ds-text="subheading">A cool subheading</h2>
<p ds-text="body">A cool paragraph</p>
By leveraging attribute selectors, we can may our CSS much more expressive. Not only is the style selector specific and scoped but also easily conveys its intent in the HTML.
CSS libraries and Design Systems can leverage more expressive selectors and Shadow DOM to create resilient CSS that works in more predictable and reliable ways. Check out the Clarity Design System Typography utilities, which leverage as well as blueprintcss.dev which both leverage similar attribute-based APIs.