I'd like to introduce a pattern for writing CSS I've been using in production for almost a year now that aims at solving the hassle of theming and coloring in CSS.
This pattern helped me reduce by 60-90% the size of my CSS code related to coloring and theming compared to libraries like Bootstrap or Semantic.
Warning : If you're a beginner, you might find this too abstract, so I advise you to read what follows only if you have already a bit of experience doing front-end design.
Introduction : context and pattern requirements
To introduce a bit what comes next, here are a few words about my view on front-end design. I've been web-designing since 1999 (starting with the infamous Microsoft Front-Page). I have written quantities of front-end code, for platforms and frameworks like Dotclear, Wordpress, Drupal, making full-fledged Django applications, React, while I still enjoy writing static sites and pure HTML+CSS. I am confortable saying I am quite familiar with not only the DOM syntax, but also with the DOM rationale and philosphy, which is my opinion amounts to kepping the three languages (HTML / CSS / JS) separated by function. As for CSS (the topic of the day), CSS should be as much as possible the only language to take care of styling. HTML shouldnt take care of CSS (avoid using style
property), JS shouldnt take care of CSS (avoid using CSS-in-JS, CSS modules, or setting properties in JS). Nevertheless these languages can be interfaced, namely (in a simplified way) HTML interfaces with CSS using classes and ids, and JS can interface with CSS setting classes and ids dynamically. Of course, there may be some exceptions to doing front-end this way, but it's a philosophy I like to stick to as much as I can, it makes my code clean, predicable and maintainable.
Let's get back to our problem. I wanted to expose a theming and coloring CSS library that fullfiled the following requirements :
- Having a large color palette that encompasses the color wheel (red, blue, pink ... green), monochrome shades (grey, black, white ...), social network shades (instagram, twitter, google), modal shades (success, error, warning)
- Storing the colors in variables, whether using preprocessor variables (SCSS) or native CSS variables (aka "CSS custom properties", also sometimes wrongly called "CSS4 variables"). So these can be customized by compiling a custom version of the library.
- The CSS color API doesn't need to know anything at all about all the other parts of my front-end. In other words, the CSS must completely be framework-agnostic (so to avoid being locked down in a way of doing things, like styled components or bootstrap.)
- Also, the CSS color API must be provided separately from any components or html code. Again, this means the color API cannot know anything about the rest of the front-end framework.
- Doing all of this in a way that respects the DOM philosphy (C in CSS means cascading, so no CSS modules or weird CSS-in-JS naive reinterpretations of what CSS should be). I just want to import a CSS file into my HTML and leverage the color API out of the box. But I also want to be able to include it in more complex React applications, for instance, and leverage it when designing components.
- Last but not least : I would consider being successful in designing such a CSS library if I manage to reduce the quantity of code necessary to color components using it from polynomial to linear. Yes, because traditional component libraries such as Bootstrap, Semantic, Material-UI, even Tailwind CSS, encourage you to write "one class per color per component". Instead, I'd like to have only "one class per component", meaning CSS component classes must be able to understand any color.
- Bonus : Making this color API expose custom themes, like a dark theme.
What we want to avoid : writing a polynomial number of classes to style components per color
Let's look at how Bootstrap works. This is very illustrative of the mainstream way of doing component coloring and theming, and libraries like Semantic-UI and Tailwind offer a similar syntax for classes.
If we look the styling of a button in the bootstrap source we have
@each $color, $value in $theme-colors {
.btn-#{$color} {
@include button-variant($value);
}
}
(bootstrap/scss/_buttons.scss)
Here, in theory the SCSS is quite neat. The code is readable and understandable. You can see that we loop through a hash of colors and define one class per color. The problem is with the compiled output.
This way of writing classes is actually very unefficient.
Why ?
Because this makes us compile to a polynomial quantity of code :
- If we have a number
i
of colors (red, green, blue, yellow ...) - and a number
n
of components (button, card, modal, ...) We would end up havingi * n
CSS classes. (Number of colors
timesnumber of components
). If we increase one or the other, this has a polynomial (> linear) effect on the size of the compiled output. This will make your CSS files huge, with all the issues that come with it (increased page loading time, browser unable to pick up what's really important to render, not to mention hard to read compiled output for debugging).
A code-efficient pattern for coloring, inspired by OOP, using CSS variables
Let's take a look at what CSS variables can do for us. For the sake of example simplicity, we'll say we have 3 colors : red, blue and yellow.
First, let's store the colors in CSS variables
We store the variables according to the spec the colors in the root HTML element. Namely :
:root {
--red:#DF2F00;
--yellow:#FFDA22;
--blue:#89A6FB;
}
(main.css/main.scss)
If we're using a preprocessor, we could have something like this to make it a bit more maintainable
$colors:(
"red":#DF2F00;
"yellow":#FFDA22;
"blue":#89A6FB;
)!default;
@mixin define-root-variables {
:root{
@each $color, $value in $colors{
--#{$color}:#{$value};
}
}
}
Then, if we don't look to improve on having the polynomial number of classes, we would write component classes like this
(I'm just adapting the bootstrap code, don't do this at home kids!)
@each $color, $value in $theme-colors {
.btn-#{$color} {
@include button-variant(var(--#{$color})); //We don't need to provide the value anymore because the CSS var takes care of it !
}
}
At the moment, this doesn't seem like a huge improvement and it is not ! ... but wait, the magic has yet to come. We're almost there.
Introducing Setters and Getter classes
Taking inspiration from the world of Object-Oriented Programming, we create helper classes which we call setters and getters, that set and read a local variable.
Setters set :root
variables to an intermediate variable that we call (at the moment, in a bit verbose way) --current-color
.
Getters consume the locally set --current-color
to apply it to an element in a particular way.
An example is worth a thousand words :
As you can see, with this approach :
- We only have to create one class per 'use-case', in the example we provided three (text color, background color, and a transparent button). This class is compatible with all the colors.
- Subsequently, the theming of html block or components only requires, for
n
components andi
colors :-
i
classes for the color setters -
n
classes for the components - =
n+i
classes which is linear.
-
Let's take a second to realize the code savings:
-
For 5 colors (
i=5
) and 10 components (n=10
), the "classic" approach would imply the creation ofi*n = 5 * 10 = 50
CSS classes, whereas the Setter/Getter pattern would only createi + n = 5 + 10 = 15
. This is a 70% reduction in the number of CSS classes needed. -
For 10 colors (
i=10
) and 20 components (n=20
), the "classic" approach would imply the creation ofi*n = 10 * 20 = 200
CSS classes, whereas the Setter/Getter pattern would only createi + n = 10 + 20 = 30
. This is a 85% reduction in the number of CSS classes needed. -
For 15 colors (
i=15
) and 100 components (n=100
), the "classic" approach would imply the creation ofi*n = 15 * 100 = 1500
CSS classes, whereas the Setter/Getter pattern would only createi + n = 15 + 100 = 115
. This is a 93% reduction in the number of CSS classes needed.
A few limitations of this approach (at this point, as we will solve most of them in the part 2) :
- The CSS classes used above as setters are quite verbose (
.current-color-red
), this can be improved before using this in production - If we wish to have more complex color interactions, such as changing color shade/tint on hover, or contrasting the text automatically, we need a slightly more complex pattern, but it's completely feasible in a linear number of classes as well. Look at the second part of the article for how to do this.
- The CSS vars are only compatible with 95%+ of browsers. This will not make it in IE11.
Conclusion
By using an intermediate class we call setter, that acts in between the component class (getter) and the :root
CSS variables, we are able to reduce the quantity of CSS classes needed to style components from a polynomial number to a linear number.
I write this hoping to give you some inspiration on how to improve your theming and coloring game. IMHO this is long overdue in 2020 !
If this helped you, I invite you to look at Swatch (docs | github ), a library that exposes this pattern in CSS with more than 80 colors. And it's only 2.5KB gzipped. If you like it, a star on github would warm our heart !
In the part 2 of the article (coming soon)
- I will introduce the CSS library Swatch (docs | github ), that exposes a coloring API based on the previous pattern. We will take a look at how to use it in just a few minutes :)
- We will see how to use this pattern with Swatches of colors (dark, normal, light, contrast), to make coloring more complex, interesting and interactive
Thank you for reading !
Top comments (2)
I really like this approach very well thought out and great article! I'll have to play around with this.
while I'm not a huge fan of css-in-js myself I like the dynamic aspect of it, I'm curious your thoughts on why to avoid it?
Thanks you for your comment and for reading ! I will write, hopefully tomorrow, a second part on how to use multi variable setters to achieve more complex looks.
Here are in my view the so-called advantages of CSS in JS :
If we debunk a bit this, and reflect on the pattern, we realize
sass src/main.scss dist/main.css -I ./node_modules -c --no-source-map && postcss dist/main.css -o dist/main.mincss
is simply as powerful, and does miracles with anodemon
script).component { padding : calc(var(--page-padding) / 2)}
. A bit more verbose, that's true (some would say ugly), but I believe that puts to good use the CSS specs. For the colors, that's a bit less obvious, but that's also why I wrote this article !This is only my modest view on it. Maybe I'm missing out on some particulars of it, or maybe I'm very old fashioned by having this view, what do you think ?