This article covered just a small sample of the different CSS techniques that you might use to create a maintainable codebase.
Single Responsibility Principle
The single responsibility principle states that everything you create should be created for a single, focused reason. The styles you apply to a given selector should be created for a single purpose and should do that single purpose extremely well.
This doesn’t mean you should have individual classes for padding-10
, font-size-20
, and color-green
. The single purpose we’re talking about is not the styles that they apply, but rather where the styles are applied. Let’s look at the following example:
<div class="calendar">
<h2 class="primary-header">This Is a Calendar Header</h2>
</div>
<div class="blog">
<h2 class="primary-header">This Is a Blog Header</h2>
</div>
<style>
.primary-header {
color: red;
font-size: 2em;
}
</style>
While the preceding example appears to be quite efficient, it has clearly broken our single responsibility principle. The class of .primary-header
is being applied to more than one, unrelated element on the page.
The “responsibility” of the primary header is now to style both the calendar header and the blog header. This means that any change to the blog header will also affect the calendar header unless you do the following:
<div class="calendar">
<h2 class="primary-header">This Is a Calendar Header</h2>
</div>
<div class="blog">
<h2 class="primary-header">This Is a Blog Header</h2>
</div>
<style>
.primary-header {
color: red;
font-size: 2em;
}
.blog .primary-header {
font-size: 2.4em;
}
</style>
This approach in our CSS techniques, while effective in the short term, brings us back to several of the problems we had at the beginning of the chapter. This new header style is now location-dependent, has multiple inheritances, and introduces a game of “winning specificity.”
A much more sustainable approach to this problem is to allow each class to have a single, focused responsibility:
<div class="calendar">
<h2 class="calendar-header">This Is a Calendar Header</h2>
</div>
<div class="blog">
<h2 class="blog-header">This Is a Blog Header</h2>
</div>
<style>
.calendar-header {
color: red;
font-size: 2em;
}
.blog-header {
color: red;
}
</style>
While it’s true that this approach can cause some duplication (declaring the color red twice), the gains in sustainability greatly outweigh any duplicated code. Not only will this additional code be a trivial increase in page weight (gzip loves repeated content), but there is no guarantee that the blog header will remain red, and enforcing the single responsibility principle throughout your project will ensure that further changes to the blog header are done with little work or possible regressions.
Single Source of Truth
The single source of truth approach takes the single responsibility theory to the next level in that not only is a class created for a single purpose, but also the styles applied to that class come from one single source. In a modular design, the design of any component must be determined by the component itself, and never imposed on it by a parent class.
Let’s take a look at this in action:
<div class="calendar">
<h2 class="calendar-header">This Is a Calendar Header</h2>
</div>
<div class="blog">
<h2 class="blog-header">This Is a Blog Header</h2>
</div>
<style>
.calendar-header {
color: red;
font-size: 2em;
}
/* blog.css */
.blog-header {
color: red;
font-size: 2.4em;
}
.blog .calendar-header {
font-size: 1.6em;
}
</style>
The intention of these styles is to decrease the size of the calendar header when it is inside a blog article.
From a design standpoint, that might make perfect sense, and what you end up with is a calendar component that changes appearance depending on where it is placed.
This conditional styling is what I like to call a “context,” and is something I use quite extensively throughout my design systems.
The main problem with this approach is that the decreased font size originates from the blog component, and not from within the calendar’s single source of truth, the calendar component file.
In this case, the truth is scattered across multiple component files. The problem with having multiple sources of truth is that it makes it very difficult to anticipate how a component is going to look placed on the page. To mitigate this problem, I suggest moving the contextual style into the calendar module code:
<div class="calendar">
<h2 class="calendar-header">This Is a Calendar Header</h2>
</div>
<div class="blog">
<h2 class="blog-header">This Is a Blog Header</h2>
</div>
<style>
.calendar-header {
color: red;
font-size: 2em;
}
.blog .calendar-header {
font-size: 1.6em;
}
/* blog.css */
.blog-header {
color: red;
font-size: 2.4em;
}
</style>
With this approach, we are still able to decrease the size of the calendar header when it is inside of a blog article, but by placing all of the calendar-header
contextual styles into the calendar file, we can see all of the possible variations of the calendar header in a single location.
This makes updating the calendar module easier (as we know all of the conditions in which it might change) and allows us to create proper test coverage for each of the variations.
Component Modifiers
While the single source of truth approach does improve clarity by placing the context inside of the component file, it can become difficult to keep track of several different contexts. If you find that the calendar header is smaller inside of dozens of different contexts, it might be time to switch from contextual modifiers to modifier classes.
Component modifiers (also called skins or subcomponents, depending on the methodology you subscribe to) allow you to create multiple variations of a component to be used in various circumstances. They work in a very similar way to contexts, but the qualifying class is part of the component rather than a parent of the component:
<div class="blog">
<h2 class="blog-header">This Is a Blog Header</h2>
...
<div class="calendar calendar--nested">
<h2 class="calendar-header">This Is a Calendar Header</h2>
</div>
</div>
<style>
/* calendar.css */
.calendar-header {
color: red;
font-size: 2em;
}
.calendar--nested .calendar-header {
font-size: 1.6em;
}
/* blog.css */
.blog-header {
color: red;
font-size: 2.4em;
}
</style>
In the preceding example, we have created a calendar--nested
modifier using the traditional BEM syntax. This class by itself does nothing, but when it is applied to the calendar component, the elements inside of the component can use it as a local context and change their appearance.
With this approach, we can use this modified calendar skin whenever we want, and we will get that smaller header (along with other changes if we want). This keeps all of your component variations in a single file and allows you to use them (or not use them) whenever you need them, not making them dependent on some random parent class.
Conclusion
This article covered just a small sample of the different CSS techniques that you might use to create a maintainable codebase, including the following:
- Separating container from content
- Defining roles and responsibilities of layouts versus components
- Using single, flat selectors on all of your markup
- Using other principles, such as the single responsibility principle, single source of truth, and content modifiers
The suggestions outlined here benefit projects of any shape or size, but it is ultimately up to you and your team to decide how you are going to write your CSS techniques.
The only requirement I have is that you have these conversations, that you set expectations, and that you hold one another accountable during code review. If you do this, you will have cemented the core pillar of your frontend architecture and set your team up for success.