Cliff Pyles contributed to this post.
Setting CSS variables to theme a design system can be tricky: if they are too scoped, the system will lose consistency. If they are too global, you lose granularity.
Maybe we can fix both issues. I’d like to try to boil design system variables down to two types: Global and Component variables. Global variables will give us consistency across components. Component variables will give us granularity and isolation. Let me show you how to do it by taking a fairly simple component as an example.
Heads up, I’ll be using CSS variables for this article but the concept applies to preprocessor variables as well.
Global-scoped variables
System-wide variables are general concepts defined to keep consistency across your components.
Starting with an .alert
component as an example, let’s say we want to keep consistency for all of our spaces on margins and paddings. We can first define global spacers:
:root {
--spacer-sm: .5rem;
--spacer-md: 1rem;
--spacer-lg: 2rem;
}
And then use on our components:
/* Defines the btn component */
.btn {
padding: var(--spacer-sm) var(--spacer-md);
}
/* Defines the alert component */
.alert {
padding: var(--spacer-sm) var(--spacer-md);
}
The main benefits of this approach are:
- It generates a single source of truth for spacers, and a single point for the author using our system to customize it.
- It achieves consistency since every component follows the same spacing.
- It produces a common point of reference for designers and developers to work from. As long as the designers follow the same spacing restrictions, the translation to code is seamless.
But it also presents a few problems:
- The system loses modularity by generating a dependency tree. Since components depend on global variables, they are no longer isolated.
- It doesn’t allow authors to customize a single component without overwriting the CSS. For example, to change the padding of the alert without generating a system wide shift, they’d have to overwrite the alert component:
.alert {
padding-left: 1rem;
padding-right: 1rem;
}
Chris Coyier explains the idea of theming with global variables using custom elements in this article.
Component-scoped variables
As Robin Rendle explain in his article, component variables are scoped to each module. If we generate the alert with these variables, we’d get:
.alert {
--alert-color: #222;
color: var(--alert-color);
border-color: var(--alert-color);
}
The main advantages are:
- It creates a modular system with isolated components.
- Authors get granular control over components without overwriting them. They’d just redefine the value of the variable.
There is no way to keep consistency across components or to make a system-wide change following this method.
Let’s see how we can get the best of both worlds!
The two-tier theming system
The solution is a two-layer theming system where global variables always inform component variables. Each one of those layers follow a set of very specific rules.
First tier: Global variables
The main reason to have global variables is to maintain consistency, and they adhere to these rules:
- They are prefixed with the word
global
and follow the formula--global--concept--modifier--state--PropertyCamelCase
- a
concept
is something like aspacer
ormain-title
- a
state
is something likehover
, orexpanded
- a
modifier
is something likesm
, orlg
- and a
PropertyCamelCase
is something likeBackgroundColor
orFontSize
- a
- They are concepts, never tied to an element or component
- this is wrong:
--global-h1-font-size
- this is right:
--global--main-title--FontSize
- this is wrong:
For example, a global variable setup would look like:
:root {
/* --global--concept--size */
--global--spacer--sm: .5rem;
--global--spacer--md: 1rem;
--global--spacer--lg: 2rem;
/* --global--concept--PropertyCamelCase */
--global--main-title--FontSize: 2rem;
--global--secondary-title--FontSize: 1.8rem;
--global--body--FontSize: 1rem;
/* --global--state--PropertyCamelCase */
--global--hover--BackgroundColor: #ccc;
}
Second tier: Component variables
The second layer is scoped to theme-able component properties and follow these rules:
- Assuming we are writing BEM, they follow this formula:
--block__element--modifier--state--PropertyCamelCase
- The
block__element--modifier
the selector name is something likealert__actions
oralert--primary
- a
state
is something likehover
oractive
- and if you are not writing BEM class names the same principles apply, just replace the
block__element--modifier
with yourclassname
- The
- The value of component scoped variables is always defined by a global variable
- A component variable always has a default value as a fallback in case the component doesn’t have the dependency on the global variables
For example:
.alert {
/* Component scoped variables are always defined by global variables */
--alert--Padding: var(--global--spacer--md);
--alert--primary--BackgroundColor: var(--global--primary-color);
--alert__title--FontSize: var(--global--secondary-title--FontSize);
/* --block--PropertyCamelCase */
padding: var(--alert--Padding, 1rem); /* Sets the fallback to 1rem. */
}
/* --block--state--PropertyCamelCase */
.alert--primary {
background-color: var(--alert--primary--BackgroundColor, #ccc);
}
/* --block__element--PropertyCamelCase */
.alert__title {
font-size: var(--alert__title--FontSize, 1.8rem);
}
You’ll notice that we are defining locally-scoped variables with global variables. This is key for the system to work since it allows authors to theme the system as a whole. For example, if they want to change the primary color across all components they just need to redefine --global--primary-color
.
On the other hand, each component variable has a default value so a component can stand on its own, it doesn’t depend on anything and authors can use it in isolation.
This setup allows for consistency across components, it generates a common language between designers and developers since we can set the same global variables in Sketch as bumpers for designers, and it gives granular control to authors.
Why does this system work?
In an ideal world, we as creators of a design system, expect “authors” or users of our system to implement it without modifications, but of course, the world is not ideal, and that never happens.
If we allow authors to easily theme the system without having to overwrite CSS, we’ll not only make their lives easier but also reduce the risk of breaking modules. At the end of the day, a maintainable system is a good system.
The two-tier theming system generates modular and isolated components where authors have the possibility to customize them at a global and at a component level. For example:
:root {
/* Changes the secondary title size across the system */
--global--secondary-title--FontSize: 2rem;
}
.alert {
/* Changes the padding on the alert only */
--alert--Padding: 3rem;
}
What values should become variables?
CSS variables open windows to the code. The more we allow authors in, the more vulnerable the system is to implementation issues.
To keep consistency, set global variables for everything except layout values; you wouldn’t want authors to break the layout. And as a general rule, I’d recommend allowing access to components for everything you are willing to give support.
For the next version of PatternFly, an open-source design system I work on, we’ll allow customization for almost everything that’s not layout related: colors, spacer, typography treatment, shadows, etc.
Putting everything together
To show this concept in action I’ve created a CodePen project.
Global variables are nestled in _global-variables.scss
. They are the base to keep consistency across the system and will allow the author to make global changes.
There are two components: alert
and button
. They are isolated and modular entities with scoped variables that allow authors to fine-tune components.
Remember that authors will use our system as a dependency in their project. By letting them modify the look and feel of the system through CSS variables, we are creating a solid code base that’s easier to maintain for the creators of the system and better to implement, modify, and upgrade to authors using the system.
For example, if an author wants to:
- change the
primary
color to pink across the system; - change the
danger
color to orange just on the buttons; - and change the padding left to
2.3rem
only on the alert…
…then this is how it’s done:
:root {
/* Changes the primary color on both the alert and the button */
--global--primary--Color: hotpink;
}
.button {
/* Changes the danger color on the button only without affecting the alert */
--button--danger--BackgroundColor: orange;
--button--danger--hover--BorderColor: darkgoldenrod;
}
.alert {
/* Changes the padding left on the alert only without affecting the button */
--alert--PaddingLeft: 2.3rem;
}
The design system code base is intact and it’s just a better dependency to have.
I am aware that this is just one way to do it and I am sure there are other ways to successfully set up variables on a system. Please let me know what you think in the comments or send me a tweet. I’d love to hear about what you are doing and learn from it.
We use exactly this pattern for our design system’s component library, but in CSS-in-JS (glamorous, in our case) rather than CSS/Sass/BEM. It’s worked well for us, and our consumers have appreciated the flexibility of customization.
For those interested: our global theme, a component’s local theme, and that component’s source.
I don’t agree that local theme variables must reference a global theme variable, though. While the exception, there are cases where a local variable can just be a direct value in our system, and I’d imagine that’s a shared need.
caniuse.com says no support for IE11. Unfortunately, my company has said to us developers to support even IE11. They basically want almost anyone to have the same browsing experience.
I love this concept of CSS variables and theming but is there any way to use this or something similar or will I have to have fallbacks for IE thereby defeating the purpose.
I’d love to hear more on this subject.
I’m not sure if this helps you out or not, Jake, but you can use this concept with SASS which should enable you to work within the bounds of IE11.
Hi Jake, although the author has specifically used CSS variables here, you’ll notice that they stated these concepts work across pre-processors such as LESS, SASS, or PostCSS too. :)
Use SCSS (Sass). I don’t see Sass going anywhere soon, so it’s worth the investment.
Unless you want to be able to customize the theme in front end in real time, you don’t necessarily need css variables. The same concept can be applied to any css pre-processor with variables such as SASS and LESS. The whole idea is that you use variables to help keep consistency in the theme.
What about using PostCSS with http://cssnext.io for example?It seems to me that allows to help you use css variables even into ie9.That aproach requires just one additional step for compile your CSS in difference with native CSS variables.But allows to use all above described technics
And if you don’t want to use SASS or any other pre-processor, use PostCSS instead. E.g. CSSNext – http://cssnext.io/features/#custom-properties-var
Thanks for the replies regarding IE support. I just started learning how to use SASS anyway since my employer has talked about us starting to use it. I’m also going to look at that PostCSS too that Scott, Pavel, and basher all suggested. I just looked at cssnext page and it looks interesting, too.
Thanks!
Awesome! Quick question: Why are the fallback values not at the component variable definitions, but wherever they’re used?
Originally we ran into some issues with browser support when trying to declare a custom property, and using another custom property with a default value. I just did a quick check though, and I wasn’t able to reproduce the issue, so that might be a good way to go, but I would verify your target browsers play well with it first.
Pavel, I totally agree with you. I was realized this approach on the https://webdesignblog.info/
Without this, I could not support ie9. CSSnext solves this task.
Setting CSS variables to theme a design system can be tricky: if they are too scoped, the system will lose consistency. If they are too global, you lose granularity.
https://run3coolmath.io/