Inheritance Theming with CSS Properties
Published by Maureen Holland
I've seen a huge benefit in switching from selector-based to inheritance-based theming at work. CSS custom properties have been the key to unlocking a more flexible and scalable system.
Selector-based theming
If you've ever worked with BEM, this pattern will be familiar to you. The --[modifier]
part is used to change a block's appearance.
/* button.css */
.button--1 {
background-color: red;
}
.button--2 {
background-color: yellow;
}
.button--3 {
background-color: pink;
}
.button--4 {
background-color: green;
}
I've called this the "selector-based" approach because it's very selector-heavy. This becomes troublesome when there are many themes that need to be applied to many components for two reasons:
- It multiples the number of modifiers you need. If you have 7 themes across 3 components, you need 21 modifier selector classes.
- It lacks a source of truth. We're asking components to individually manage color palettes when we really want all components to reference the same palette.
Inheritance-based theming
Think of the onClick
prop in a React Button
component. A button always does something on click, but it doesn't need to know every possible thing that could be. We pass that information through the prop.
Similarly, a button always has a color palette, but it doesn't need to know every possible palette that could be. We pass that information through the custom property.
Custom properties define shared properties according to a higher level theme selector. This is essentially the Material UI design system's context approach, where custom properties used in component styles are "system" tokens and themed values are "reference" tokens.
/* themes.css: define themes */
.theme--1 {
--primary-color: red;
}
.theme--2 {
--primary-color: yellow;
}
.theme--3 {
--primary-color: pink;
}
.theme--4 {
--primary-color: green;
}
// button.css: use themed value
.button {
background-color: var(--primary-color);
}
Inheritance-based theming allows us to have a constant number of theme selector modifiers, regardless of how many components they affect, and those themes are all managed from one file.
Comparison
Using the above code snippets as a base, consider how each approach would adapt to the following requests.
Theme 2 more components
Changes required in selector approach:
- Add 4 link modifiers
- Add 4 icon modifiers
- Update any links and icons in markup to use the new modifier selectors
Changes required in inheritance approach:
- Update link component to use themed property
- Update icon component to use themed property
Add 3 new themes
Changes required in selector approach:
- Add 3 button modifiers
- Add 3 link modifiers
- Add 3 icon modifiers
Changes required in inheritance approach:
- Add 3 new themes in themes file
If you want to explore the final code from each approach after those changes, check out this codepen:
In the selector approach, we end up with 21 modifier selector classes across 3 files. In the inheritance approach, we end up with 7 modifier selector classes in 1 file.
Theming needs vary. Sure, you might have some themes for established products meant to live forever. But you also might have some for marketing campaigns meant to die after a week. Or experimental products that tweak their brand colors regularly.
The extensibility and centralization of inheritance-based theming makes it quick and easy to fulfill new requests and clean up retired themes. If you haven't tried this approach before, I highly recommend it.
More resources:
- Keith J. Grant's A structured approach to custom properties
- Set Studio's How we're approaching theming with modern CSS
- Max Böck's Color Theme Switcher (the inspiration for garden bird themes on this blog)
- Lea Verou's Proposal for Variable Groups aims to address a common pain point for design systems