A commentary on excerpts from the book "Frontend Architecture for Design Systems"
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>
.primary-header {
color: red;
font-size: 2em;
}
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:
.primary-header {
color: red;
font-size: 2em;
}
.blog .primary-header {
font-size: 2.4em;
}
This approach, while effective in the short term ... [means the] 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>
.calendar-header {
color: red;
font-size: 2em;
}
.blog-header {
color: red;
font-size: 2.4em;
}
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.
If you find that your design does have only a handful of "acceptable" heading styles, you can use mixins to capture the code that defines one e.g. @mixin primary-header
and then apply it to any context where that set of styles should apply.
.calendar-header {
@include primary-header;
}
.blog-header {
@include primary-header;
}
This makes it easy to update styles to multiple components that should always look consistent with each other by writing the code in one place, and implementing it across your code base, rather than having to search for all of the use-cases and changing a dozen instances, or worse, missing one and having a fracture in your design implementation.
If you find that you need to change one instance, e.g. the blog (from our example above) the mixin should indicate to the developer that this style is used in multiple places across the codebase! It is their responsibility to identify each implementation of the mixin and identify whether they should all change, or if there is a new design pattern to be captured.
If, for example, the blog heading should be identical to the rest of the content type headings, except that it's blue, and bold (for emphasis) the blog heading should no longer implement the primary-header
mixin, and instead define its styles directly in its css file. Even if that means duplicating other css.
If, on the other hand, all of the headings should be updated to match a design refresh, then the mixin is the appropriate place to add the new styles so that every heading that is intended to look the same, will continue to do so.
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="blog">
<h2 class="blog-header">This Is a Blog Header</h2>
...
<div class="calendar">
<h2 class="calendar-header">This Is a Calendar Header</h2>
</div>
</div>
/* calendar.css */
.calendar-header {
/* <--- This is inside the calendar file */
color: red;
font-size: 2em;
}
/* blog.css */
.blog-header {
color: red;
font-size: 2.4em;
}
.blog .calendar-header {
/* <--- This is inside the blog file */
font-size: 1.6em;
}
The intention of these styles is to decrease the size of the calendar header when it is inside of 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="blog">
<h2 class="blog-header">This Is a Blog Header</h2>
...
<div class="calendar">
<h2 class="calendar-header">This Is a Calendar Header</h2>
</div>
</div>
/* calendar.css */
.calendar-header {
color: red;
font-size: 2em;
}
.blog .calendar-header {
/* <--- This is inside the calendar file */
font-size: 1.6em;
}
/* blog.css */
.blog-header {
color: red;
font-size: 2.4em;
}
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.
I'm personally not a huge fan of using "implementation-specific" contexts e.g. "the calendar header when it's specifically inside a blog" .blog .calendar-header
because it gets very bespoke, and you develop a lot of "snowflake" implementations, perhaps without realizing it. I'm a much bigger fan of using "semantic" contexts, usually in the form of a modifier on the parent component or some higher level declaration e.g. [data-theme="dark"]
set on the body to indicate the site should be displayed using the "dark mode" styles.
We'll actually touch on this in the following sections, I just wanted to mention it now since the provided code examples could be seen to recommend the "implementation-specific" context.
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>
/* 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;
}
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, not making them dependent on some random parent class.
I much prefer this approach to the one from the last section because the calendar--nested .calendar-heading
styles can be applied to the "blog" context, as well as the "news" context, and any number of others without having to define each of them explicitly in the calendar.css
file. The markup simply says "there's a calendar here, and it's nested inside something, so adjust things accordingly." In our example, it's just the heading styles, but it could affect any number of things about how the calendar looks.
A personal preferences I've developed is to use classes for purely "stylistic" definitions, and data-attributes for "semantic" meaning. So, in our example above, I see the .calendar--nested
class to have a semantic meaning of something along the lines of "There's a calendar nested inside something else" as opposed to a purely "stylistic" class of something like .calendar--small
which might be used to create a small variation of a calendar that may be nested inside something, or might not.
Here's an example of how I might define the semantic version:
/* calendar.css */
.calendar-header {
color: red;
font-size: 2em;
}
[data-nested="true"] {
.calendar-header {
font-size: 1.6em;
}
}
The two could accomplish exactly the same thing (making the heading smaller, etc.) but the class version defines a variation of the calendar independent of context, while the other has a semantic meaning that describes how the calendar is being used.
Another thing to note is that the [data-nested="true"]
attribute could be applied to the .blog
wrapping element, rather than the calendar element, and then any component that is displayed inside the blog, can opt-in to having different styles applied in that context. And to take that one-step further, the data attribute could also be applied to the .news
wrapping element and then a calendar inside that would automatically use the "nested" styles as well.
In some circles, the single responsibility principle, applied to CSS, means that each class has a small, focused responsibility, and that one class will set the box model properties of an element, while another sets the typography, and a third sets the color and background.
For our system and set of rules, the single responsibility principle means that every class I create is made to be used for a single purpose, in a single place. Therefore, if I make a change to .rh-standard-band-title
, I can be confident that the only effect this will have on our site is to change the appearance of the title inside of the rh-standard-band
.
This also means that if we decide to deprecate rh-standard-band
, we can completely remove all of the associated CSS without fear of breaking some other component that “hijacked inheritance” and became reliant on that CSS. It’s because of this desire to not “use and abuse the cascade” that I made sure that every class is used only for the single purpose for which it was created.
Yep. Agree.
Once we have a page full of single-classed elements we can be pretty confident that our changes to .rh-standard-band-title
won’t affect any other part of the system, but what is to say that our .rh-standard-band-title
can’t be affected by something else? This is why it is so important to maintain a single source of truth for every component, and by extension, every element on the page. This means not relying on any H2 styles, or “header H2,” or any other selector outside of the Standard Band Sass file to style this element.
This is not to say that our title can never be altered or modified by an outside force. What this does mean is that anything that these modifiers or contexts do to an element will be defined in the same place as the element’s original styles, not in some other location. So while I have no objections to .some-context .rh-standard-band-title
, these styles will always be defined in the Standard Band Sass partial, and never anywhere else.
Again, I agree with this.
As I already mentioned, I have no objections to having modifiers on my components, but in every single instance these modifications need to be opt-in. What this means is that if I define Modifier A for Component B, then Modifier A would have no effect on any Component C unless it opted in to that modifier.
Before we dive into an example, let me explain one architectural choice I made in regards to modifiers and contexts. While BEM, SMACSS, and OOCSS all have conventions for modifiers, themes, or skins, they all require adding modifying classes to the block or element. I decided to take a different approach that wouldn’t require any additional classes. I was really determined to, as Ben Frain puts it, “let the thing be the thing.” I never wanted anyone to confuse the “thing’s” class and its modifier. So I decided that all modifiers and contexts would be put inside data attributes instead, like this:
<div class="foo" data-bar="baz">...</div>
This separation had another benefit beyond distinguishing purpose and role. Classes are very one-dimensional: either the class is present or it is not. A data attribute, on the other hand, is two-dimensional, having the attribute itself and the value passed into it. To compensate for the missing dimension, you’ll often find classes using a namespace to define which group they belong to. A data attribute has an explicit namespace, and can therefore pass any necessary value into it. It may be a few more characters, but using data attributes makes it really obvious that our component has a property of data-align
and that it can be set to a variety of values.
This took me a while to chew on and decide whether I agreed with this approach or not. At first, I wasn't sure, and thought, "If I have a 'Basic Card' where the image is always stacked on top of the content, and a 'Flexible Card' where at some breakpoint, the content flows beside the image, why not have a .card
class, and then some additional styles inside .card--flexible
to change the "flow" of the card layout etc. Something like:
.card {
// Styles applicable to all variations go here. e.g.:
display: flex;
flex-direction: column; // At the smallest breakpoint, all of our cards will be "vertical"
&--flexible {
@media (min-width: 700px) {
flex-direction: row;
}
}
}
That's how I've done things in the past. And it's worked. I think where the data attribute shines is when there are multiple aspects of the card that need to change to support multiple variations, and some of those variations share things in common. Let's say we have the following in our designs:
Basic Card
- Normal Heading
- Vertical Layout
- Text Link
CTA (Call to action) Card
- Strong Heading
- Vertical Layout
- Button Link (e.g. visually styled to look like a button, but uses the
<a>
tag)
Flexible Card
- Normal Heading
- Flexible Layout
- Text Link
Flexible CTA Card
- Strong Heading
- Flexible Layout
- Button Link (again, visual only)
In this scenario, using the modifier methodology, we'd define two variations to capture the differences. Something like:
.card {
display: flex;
flex-direction: column;
&--flexible {
@media (min-width: 700px) {
flex-direction: row; // Layout change for Flexible cards
}
}
&__heading {
font-weight: 400; // Default heading font-weight
}
&__link {
text-decoration: underline; // Or whatever a generic text links look like
}
&--cta {
.card__heading {
font-weight: 700; // CTA heading font-weight
}
.card__link {
@include button; // This is a mixin that provides the button styles for the `<a>` tag
}
}
}
Then, we can pass either modifier to achieve .card--cta
or .card--flexible
, or achieve the "Flexible CTA" version by adding both of the previous modifiers to the card. e.g. <div class="card card--cta card--flexible">
. This works, and is why I've always done it in the past.
What's different about the data-attributes approach is that we get context about what is going to change, as well as how it changes. By just passing the --cta
modifier, we achieve what we want, but reading only the markup, it's not clear what is going to change. Is it just the link style? Is it going to give the card a drop-shadow? etc.
Let's take a look one possible implementation using data-attributes:
.card {
display: flex;
flex-direction: column;
&[data-card-layout="flexible"] {
@media (min-width: 700px) {
flex-direction: row;
}
}
&__heading {
font-weight: 400; // Default heading font-weight
[data-card-volume="cta"] & {
font-weight: 700; // CTA heading font-weight
}
}
&__link {
text-decoration: underline; // Or whatever a generic text links look like
[data-card-volume="cta"] & {
@include button; // This is a mixin that provides the button styles for the `<a>` tag
}
}
}
Now, when we see the markup <div class="card" data-card-volume="cta" data-card-layout="flexible">
it's clear that the visual "volume" of the card is the "cta" version, (as opposed to the default, or maybe a "subtle" version that has a lighter heading, and smaller text link). We can also clearly see that the "layout" is going to be flexible.
This two-dimensional nature of the attribute/value pair tells us why the styles are changing which is far more useful than a bunch of classes that have no implicit context.
One final reason data-attributes are highly useful is because they can provide context to a variety of components without having to actually affect the markup of any of the specific components. Let's say we have a card, that has a light background, dark text, and a dark icon. If we need to support a "dark mode" for our site, those colors may not cut it. Let's take a look at how that could be addressed.
<body data-theme="light">
<!-- or no "data-theme", by default -->
...
<div class="card">
<svg class="card__icon"></svg>
<!-- This is the icon -->
<div class="card__heading">This is the card heading</div>
...
</div>
...
</body>
The accompanying css might look something like this:
.card {
background: $white;
&__icon {
color: $black;
}
&__heading {
color: $gray; // Let's assume this version of gray is accessible over both white and black backgrounds
}
}
That's great, but now we set data-theme="dark"
on the <body>
element, and those bright white cards are visually distracting. We can make them "Opt-in" to the "dark mode" by doing something like this:
.card {
background: $white;
[data-theme="dark"] & {
background: $black;
}
&__icon {
color: $black;
[data-theme="dark"] & {
color: $gray-light;
}
}
&__heading {
color: $gray; // Notice, we don't need to provide a variation for the Heading because, in our example, the color works over both backgrounds
}
}
This means that if [data-theme="dark"]
is set on any parent of the card, including the top-level element <body>
the cards will change their colors to match the rest of the site accordingly. In this case, we didn't actually set a variation, or a data-attribute directly on the card. We set it on <body>
and then the card (as well as every other component) can define alternate styles, if needed. This demonstrates the ability to have "local" and "global" contexts by setting attributes on components for "local" context, or on something like <body>
for "global" context.
Finally, you can use a data-attribute to provide both local, and global context. For example, let's say we have a "section" component that has a colored background, a heading, and then a grid of cards inside. We need to support two versions
- A dark background with a light "section heading"
- A light background with a dark "section heading.
The question is, "What do the cards look like"?
Note: This can quickly get very complicated when you want to support "contrasting" components like this for both "light" and "dark" modes of a site. You have to think through "If I say this background is 'dark' does it contextually switch to 'light' when the data-theme
is switched from light
to dark
on <body>
?" and other variations... So, to keep things simple for the sake of this article, let's assume you only have one "mode" for the site (e.g. no global light/dark switching) but you do have "sections" that can have dark backgrounds to provide emphasis.
We can define the section like this:
<div class="section" data-theme="light">
<h2 class="section__heading">
<div class="card-grid">
<!-- Cards go here -->
</div>
</div>
(In our CMS, or whatever produces our code, we would be able to toggle the data-theme
from "light" to "dark" however makes the most sense.)
Then the css for the section would look like this:
.section {
&[data-theme="light"] {
background: $white;
}
&[data-theme="dark"] {
background: $black;
}
&__heading {
[data-theme="light"] & {
background: $black;
}
[data-theme="dark"] & {
background: $white;
}
}
}
So that sets the local context for the section so that the background and heading color are set appropriately. But what's magical, is that this also sets the "global" (or perhaps a better word is "regional", since it's only within this section) context for the cards inside it. So we can use the exact same css that we used earlier, and if the section is "dark" the cards will be "dark". If the section is "light", the cards will be "light". And each section can set the attribute once, and anything that's placed inside the section can "opt-in" to adapting its styles for the given context.