DRY CSS is a lie
Last week I had a discussion with a colleague of mine about different ways to share styles in CSS. I’ve already written about how to write maintainable CSS, but since this is still quite controversial for many I’d like to elaborate my point from a different perspective.
#How to make things look the same
Let’s say the design you implement uses rounded boxes as a visual element.
The code requirements are simple:
- Boxes should look the same everywhere.
- There can be slight variations of the design.
- Changing the design of the box or any of its variations should update only the intended variation everywhere.
#Just using classes doesn’t work
The first solution one might come up with is creating a class.
It might be called .box
. Depending on how you structure your views, this class
then is used by multiple templates or components, that are supposed to look like
this box.
.box {
border-radius: 5px;
padding: 10px;
background: white;
}
At some point somebody might want to use .box
on a form, which needs more
space. So .form
gets updated to overwrite the padding defined in .box
.
The markup might look like this:
<form class="form box"></form>
🚨 Warning! This change broke the requirements.
By overwriting styles, you’ve implicitly created a new variation of .box
. This
variation is coupled to .form
, which leads to the following problems:
- it cannot be reused by other elements not using
.form
- this dependency is not documented in the CSS (not for
.box
or.form
) - any changes to
.box
can unintentionally overwrite.form
- any changes to
.form
can unintentionally overwrite.box
Using more than one class on an HTML element strongly couples these classes together.
Updating any of them could always unintentionally affect the other. Looking at the CSS class declaration doesn’t tell you, which classes and which HTML it depends on.
#mixins, extend, composes don’t work
Sharing styles with a preprocessor is not much different to using multiple classes on the same element.
.form {
@extend .box;
padding: 0; // overwrites .box styles
}
It only makes the implicit dependency between .form
and .box
a little bit
more explicit in the CSS file.
One can now see that .form
depends on .box
, but only if you look at the
definition of .form
. Looking at the .box
class won’t tell you that it looks
different when used on a form.
It also still breaks the requirements in the same way as multiple classes, because it is still overwriting styles.
It doesn’t matter if the inherited styles are coming from a class or a mixin. The only difference is that mixins cannot be used directly in HTML.
composes
is CSS components way of overwriting styles and has the same
disadvantages. It only compiles to a different CSS output.
#OOCSS/Functional CSS does not work
The great thing about OOCSS is, that it taught us how to create variations of a visual element by using modifiers. Instead of overwriting a class one creates a new modifier class.
<form class="box box--more-space form"></form>
Instead of creating a variation of .box
by having .form
overwrite it, we’ve
created a new class, which explicitly is a variation of .box
.
Since .form
and .box
are still used on the same HTML element, this does not
decouple them. .form
could still overwrite .box
or its variations.
The OOCSS naming convention cannot give you any guarantees that this won’t happen!
“Functional CSS“ is not much different to OOCSS. It basically says: treat classes as immutable. This means never overwrite styles of another class. (unless the framework also has something like base classes)
That sounds great in theory, but again does nothing to actually add any encapsulation. Hence it also cannot give you any guarantees.
#Why do these solutions not work?
The first mistake we made is trying to decouple CSS from HTML, although we always need both to paint something on the screen.
The minimum level of abstraction for a visual element on the web is a combination of CSS and HTML.
If visual elements consist of CSS and HTML, you as an author get one step closer to:
- nobody using your classes with the wrong HTML
- nobody overwriting your styles with other classes
Some people try to do the same by putting HTML as comments in CSS, but documentation is no guarantee.
The second mistake is that, none of the solutions tried to add encapsulation. Anyone can overwrite any property of any class.
As an author of a class, there is no way in CSS for you to say: All elements using this class should always look like this. Nobody can change these properties.
If we want to make things look the same, we need to protect them from looking different.
These two mistakes are the reason why DRY CSS is a lie. DRY CSS means writing fewer lines of CSS, but with the same result as copy-pasting. CSS alone without HTML and without encapsulation is not able to consistently make things look the same.
#How to safely share styles
Acknowledging our mistakes leaves us with two rules, we need to follow to safely make things look the same:
- Use a combination of CSS and HTML and maybe JS.
- Write CSS that only affects the given HTML.
The combination of CSS + HTML (+ JS) is called a component.
If you follow these rules you’ll get encapsulation and therefore the following benefits:
- All components will look the same, no matter in which context.
- Changes to a component will always update it everywhere.
- Clear documentation of your components and their variations.
- Considerably fewer bugs caused by CSS changes.
- Easy dead code elimination of CSS and HTML.
A colleague of mine at diesdas wrote and open-sourced a linter for these rules. This reduces the time spent reviewing CSS in pull requests and decreases the chance of missing files where they are not followed.