CSS Architecture and Organization
If you’ve ever worked on a CSS codebase of any size—or even a small codebase with multiple developers—you’ll have realized how difficult it is to create CSS that’s predictable, reusable, and maintainable without being bloated. With added developers often comes added complexity: longer selectors, colliding selectors, and larger CSS files.
In this chapter, we’ll explore CSS architecture and organization. First up: file organization. We’ll take a look at strategies for managing CSS across projects, or as part of your own CSS framework.
Then we’ll look at specificity. It’s a frequent pain point for CSS development, especially for teams. Specificity is the means by which browsers decide which declarations to apply. If you’ve ever wondered why all of the buttons on your site are green when you wanted some of them to be orange, this section is for you. We’ll discuss how to calculate selector specificity, and choose selectors that maximize reusability while minimizing the number of characters you’ll need.
Finally, we’ll discuss some guidelines and methodologies for writing CSS. These rules make it easier to avoid selector-naming collisions and overly specific selectors—the kinds of issues that arise when working within teams.
File Organization
Part of a good CSS architecture is file organization. A monolithic file is fine for solo developers or very small projects. For large projects—sites with multiple layouts and content types, or multiple brands under the same design umbrella—it’s smarter to use a modular approach and split your CSS across multiple files.
Splitting your CSS across files makes it easier to parcel tasks out to teams. One developer can work on typography-related styles, while another can focus on developing grid components. Teams can split work sensibly and increase overall productivity.
So what might a good file structure that splits the CSS across files look like? Here’s a structure that’s similar to one that I’ve used in projects:
typography.css
: font faces, weights, line heights, sizes, and styles for headings and body textforms.css
: styles for form controls and labelslists.css
: list-specific stylestables.css
: table-specific stylesaccordion.css
: styles for the accordion componentcards.css
: styles for the card component
CSS frameworks such as Foundation and Bootstrap use a similar approach. Both become quite granular, with separate files for progress bars, range inputs, close buttons, and tooltips. That granularity allows developers to include only the components they need for a project.
The details of how you split your CSS will depend on your own preferences and practices. If your workflow includes a pre-processor such as Sass or Less, these would be partials with a .scss
or .less
extension. You may also add a _config.scss
or _config.less
file that contains color and font variables.
Or perhaps you have a more component-centric workflow, as with the pattern-library tool Fractal, or JavaScript frameworks like React and Vue.js. You might instead opt for a single base.css
or global.css
file that smooths out browser differences, and use a separate CSS file for each pattern or component.
Something to avoid: organizing your CSS by page or view. Page-centric approaches encourage repetitious code and design inconsistencies; you probably don't need both .contact-page label
and .home-page label
rulesets. Instead, try to find common patterns or components in your site's design, and build your CSS around them.
Using multiple files during site development doesn’t necessarily mean you’ll use multiple files in production. In most cases, you’ll want to optimize CSS delivery by concatenating files, and separating critical from non-critical CSS. We discuss optimization techniques in the chapter Debugging and Optimization.
File organization is just one aspect of CSS architecture. Despite its position in this chapter, it’s actually the least important aspect. In my experience, most CSS architecture problems arise from selector choice and specificity. We’ll discuss how to avoid these issues in the next section.
Specificity
A common observation, particularly from developers who are coming to CSS from more traditional programming languages, is this: CSS has a global scope. In other words, using button
as a selector applies those declarations to every <button>
element, whether that was intended or not.
Where this begins to go awry is when we try to solve the “global” problem by using longer selectors—for example, changing button
to form button
or a
to #content a
. As your site or application adds more patterns and features, however, you may find yourself in a selectors arms race. You may add a form.location-search button
selector to override form button
or #content .sidebar a
to override #content a
.
The “global” nature of CSS is really an issue of specificity and the cascade in Cascading Style Sheets. Although it may seem arbitrary at first, CSS has well-defined rules for determining what declarations to apply. Understanding specificity may be what separates good CSS developers from CSS masters.
Think of specificity as a score or rank that determines which style declarations are applied to an element. The universal selector (*
) has a low degree of specificity. ID selectors have a high degree. Descendant selectors such as p img
and child selectors such as .panel > h2
are more specific than type selectors such as p
, img
, or h1
. Class names fall somewhere in the middle.
Higher-specificity selectors are higher priority selectors. Declarations attached to high specificity selectors are the what the browser ultimately applies.
Calculating exact specificity values seems tricky at first. As explained in the Selectors Level 3 specification, you need to:
- count the number of ID selectors in the selector (= A)
- count the number of class selectors, attribute selectors, and pseudo-classes in the selector (= B)
- count the number of type selectors and pseudo-elements in the selector (= C)
- ignore the universal selector
If you're like me, you may have to stare at these rules for a while before they make sense.
A, B, and C combine to form a final specificity value. ID selectors such as #foo
have a specificity of 1,0,0—that's one ID selector, and no class or type selectors. Attribute selectors, such as [type=email]
and class selectors such as .story
have a specificity of 0,1,0. Adding a pseudo-class such as :first-child
(for example, .story:first-child
) gives us a specificity of 0,2,0. But using a simple type or element selector such has h1
or p
only gives us a specificity of 0,0,1. Pseudo-elements such as ::before
and ::after
are as specific as type selectors (0,0,1).
Learning More About Specificity
Keegan Street’s Specificity Calculator and Joshua Peek’s CSS Explain are helpful for learning about and calculating selector specificity.
When two selectors are equally specific, the cascade kicks in, and the last rule wins. Here’s an example:
a:link { color: #369;}a.external { color: #f60;}
Both a:link
and a.external
have a specificity value of 0,1,1—zero ID selectors, one class or pseudo-class, and one type (or element) selector. However, the a.external
ruleset follows the a:link
ruleset. As a result, a.external
will take precedence. In this case, most of our links will be cadet blue. But those with class="external"
will be orange.
Complex and combinator selectors, of course, give us higher specificity values. Consider the following CSS:
ul#story-list > .book-review { color: #0c0;}#story-list > .book-review { color: #f60;}
Although these rulesets look similar, they aren’t the same. The first selector, ul#story-list > .bookreview
, contains a type selector (ul
), an ID selector, (#story-list
), and a class selector (.bookreview
). It has a specificity value of 1,1,1. The second selector, #story-list > .book-review
only contains an ID and a class selector. Its specificity value is 1,1,0. Even though our #story-list > .book-review
rule follows ul#story-list > .bookreview
, the higher specificity of the former means that those elements with a .book-review
class will be green rather than orange.
Choose Low-specificity Selectors
Err on the side of using low-specificity selectors. They make it easier to reuse your CSS, and they extend patterns in new ways.
Consider the following:
button[type=button] { background: #333; border: 3px solid #333; border-radius: 100px; color: white; line-height: 1; font-size: 2rem; padding: .5rem 1rem;}
This gives us a charcoal gray button with white text, and rounded ends, as shown in the following image:
Let’s add some styles for a close button. We’ll use a .close
class, as shown below:
button[type=button] { background: #333; border: 3px solid #333; border-radius: 100px; color: white; line-height: 1; font-size: 2rem; padding: .5rem;}.close { width: 3rem; height: 3rem; background: #c00; border: 0; border-bottom: 5px solid #c00; font-size: 3rem; line-height: 0; padding: 0;}
Now we have two charcoal gray buttons with white text and rounded ends:
What’s happening? Our button[type=button]
selector has a specificity of 0,1,1. However, .close
is a class selector. Its specificity is only 0,1,0. As a result, most of our .close
rules do not get applied to <button type="button" class="close">
.
We can ensure that our .close
styles are applied by either:
- changing
.close
tobutton[type=button].close
- making
button[type=button]
less specific
The second option adds fewer bytes, so that’s what we’ll use:
[type=button] { background: #333; border: 3px solid #333; border-radius: 100px; color: white; line-height: 1; font-size: 2rem; padding: .5rem;}.close { width: 3rem; height: 3rem; background: #c00; border: 0; border-bottom: 5px solid #c00; font-size: 3rem; line-height: 0;}
Changing the specificity of our selector leaves us with our intended result:
Avoid Chaining Class Selectors
Another way to minimize specificity is to avoid chaining class selectors. Selectors such as .message.warning
have a specificity of 0,2,0. Higher specificity means they’re hard to override. What’s more, chaining classes may cause side effects. Here’s an example:
.message { background: #eee; border: 2px solid #333; border-radius: 1em; padding: 1em;}.message.error { background: #f30; color: #fff;}.error { background: #ff0; border-color: #fc0;}
Using <p class="message">
with this CSS gives us a nice gray box with a dark gray border:
Using <p class="message error">
, however, gives us the background of .message.error
and the border of .error
shown below.
The only way to override a chained class selector is to use an even more specific selector. To be rid of the yellow border, we’d need to add a class name or type selector to the chain: .message.warning.exception
or div.message.warning
. It’s more expedient to create a new class instead.
If you do find yourself chaining selectors, go back to the drawing board. Either the design has inconsistencies, or you’re chaining prematurely in an attempt to prevent problems you don’t have. The maintenance headaches you’ll prevent and the reusability you’ll gain are worth it.
Avoid Using id
Selectors
HTML only allows each identifier (that is, an id
attribute) to be used once per document. As a result, rulesets that use id
selectors are hard to repurpose. Doing so typically involves using a list of id
selectors—for example, #sidebar-feature, #sidebar-sports
, and so on.
Identifiers also have a high degree of specificity, so we’ll need longer selectors to override declarations. In the CSS that follows, we need to use #sidebar.sports
and #sidebar.local
to override the background color of #sidebar
:
#sidebar { float: right; width: 25%; background: #eee;}#sidebar.sports { background: #d5e3ff;}#sidebar.local { background: #ffcccc;}
Switching to a class selector, such as .sidebar
, lets us simplify our selector:
.sidebar { float: right; width: 25%; background: #eee;}.sports { background: #d5e3ff;}.local { background: #ffcccc;}
As well as saving us a few bytes, our .sports
and .local
rulesets can now be added to other elements.
Using an attribute selector such as [id=sidebar]
lets us get around the higher specificity of an identifier. Though it lacks the reusability of a class selector, the low specificity means that we can avoid selector chaining.
When You Want Higher Specificity
In some circumstances, you might want the higher specificity of an id
selector. For example, a network of media sites might wish to use the same navigation bar across all of its web properties. This component must be consistent across sites in the network, and should be hard to restyle. Using an id
selector reduces the chances of those styles being accidentally overridden.
Let’s discuss a selector such as #main article.sports table#stats tr:nth-child(even) td:last-child
. Not only is it absurdly long, but with a specificity of 2,3,4, it’s also not reusable. How many possible instances of this selector can there be in your markup?
Let’s make this better. We can immediately trim our selector to #stats tr:nth-child(even) td:last-child
. It’s specific enough to do the job. An even simpler approach: use a class name such as .total
. It’s a much shorter selector, and those styles aren’t limited to #stats
tables.
Minimize Nesting When Using a Preprocessor
Overly long, highly specific selectors are often caused by nested rulesets. Both Sass and Less support nested ruleset syntax, which is useful for grouping related styles and saving keystrokes. Take, for example, the following CSS:
article { margin: 2em auto;}article p { margin: 0 0 1em; font-family: 'Droid Serif','Liberation Serif',serif;}
In both Less and Sass, we can rewrite this to take advantage of nesting:
article { margin: 2em auto; p { margin: 0 0 1em; font-family: 'Droid Serif','Liberation Serif',serif; }}
This gives us a descendant selector, and the output will match the standard CSS above.
It’s also possible to nest a ruleset inside a nested ruleset. Take a look at this example:
nav { > ul { height: 1em; overflow: hidden; position: relative;
&::after { content: ' '; display: block; clear: both; } }}
Here we’ve nested styles for ::after
inside a declaration block for ul
, which itself is nested inside a nav
declaration block. When compiled, we end up with the following CSS:
nav > ul { height: 1em; overflow: hidden; position: relative;}nav > ul::after { content: ' '; display: block; clear: both;}
So far, so good. Our selectors aren’t terribly long or specific. Now let’s look at a more complex example of nesting:
article { color: #222; margin: 1em auto; width: 80%;
&.news { h1 { color: #369; font-size: 2em;
[lang]{ font-style: italic; } } }}
That doesn’t seem too egregious, right? Our [lang]
selector is only four levels deep. Well, take a look at our compiled CSS output:
article { color: #222; margin: 1em auto; width: 80%;}article.news h1 { color: #369; font-size: 2em;}article.news h1 [lang] { font-style: italic;}
Uh-oh! Now we have a couple of high-specificity selectors: article.news h1
and article.news h1[lang]
. They use more characters than necessary, and require longer and more specific selectors to override them. Mistakes like this can swell the size of our CSS when repeated across a codebase.
Neither Less nor Sass has a hard limit on how deeply rulesets can be nested. A good rule of thumb: avoid nesting your rulesets by more than three levels. Less nesting results in lower specificity and CSS that’s easier to maintain.
Use Type and Attribute Selectors with Caution
It’s good to keep specificity low, but be careful about the selectors you use to accomplish that. Type and attribute selectors can be the most bothersome.
Type selectors are element selectors such as p
, button
, and h1
. Attribute selectors include those such as [type=checkbox]
. Again, style declarations applied to these selectors will be applied to every such element across the site. Here’s an example:
button { background: #FFC107; border: 1px outset #FF9800; display: block; font: bold 16px / 1.5 sans-serif; margin: 1rem auto; width: 50%; padding: .5rem;}
This seems innocuous enough. But what if we want to create a button that’s styled differently? Let’s style a .close
button that will be used to close dialog modules:
<section class="dialog"> <button type="button" class="close">Close</button></section>
Markup Choices
We’re using section
here instead of the dialog
element, because support for dialog
is largely limited to Chromium-based browsers. Firefox’s support is incomplete, and only available when the user has changed their configuration settings.
Now we need to write CSS to override every line that we don’t want to inherit from the button
ruleset:
.close { background: #e00; border: 2px solid #fff; color: #fff; display: inline-block; margin: 0; font-size: 12px; font-weight: normal; line-height: 1; padding: 5px; border-radius: 100px; width: auto;}
We’d still need many of these declarations to override browser defaults, but what if we scope our button
styles to a .default
class instead? We can then drop the display
, font-weight
, line-height
, margin
, padding
, and width
declarations from our .close
ruleset. That’s a 23% reduction in size:
.default { background: #FFC107; border: 1px outset #FF9800; display: block; font: bold 16px / 1.5 sans-serif; margin: 1rem auto; width: 50%; padding: .5rem;}.close { background: #e00; border: 2px solid #fff; color: #fff; font-size: 12px; padding: 5px; border-radius: 100px;}
Just as importantly, avoiding type and attribute selectors reduces the risk of styling conflicts. A developer working on one module or document won’t inadvertently add a rule that creates a side effect in another module or document.
These selectors are perfectly okay for resetting and normalizing default browser styles. It’s best to avoid them otherwise. Class-name selectors will help you avoid many of these problems.
Choosing What to Name Things
When choosing class-name selectors, use semantic class names.
When we use the word semantic, we mean meaningful. Class names should describe what the rule does or the type of content it affects. Ideally, we want names that will endure changes in the design requirements. Naming things is harder than it looks.
Here are examples of what not to do: .red-text
, .blue-button
, .border-4px
, .margin10px
. What’s wrong with these? They’re too tightly coupled to the existing design choices. Using class="red-text"
to mark up an error message does work. But what happens if the design changes and error messages become black text inside orange boxes? Now your class name is inaccurate, making it tougher for you and your colleagues to understand what’s happening in the code.
A better choice in this case is to use a class name such as .alert
, .error
, or .message-error
. These names indicate how the class should be used and the kind of content (error messages) that they affect.
More on Architecture
Philip Walton discusses these and other rules in his article “ CSS Architecture ”. I also recommend Harry Roberts’ site CSS Guidelines and Nicolas Gallagher’s post “ About HTML Semantics and Front-end Architecture ” for more thoughts on CSS architecture.
We’ll now look at two methodologies for naming things in CSS. Both methods were created to improve the development process for large sites and large teams, but they work just as well for teams of one. Whether you choose one or the other, neither, or a mix of both is up to you. The point of introducing them is to help you to think through approaches for writing your own CSS.
Block-Element-Modifier (BEM)
BEM, or Block-Element-Modifier, is a methodology, a naming system, and a suite of related tools. Created at Yandex, BEM was designed for rapid development by sizable development teams. In this section, we’ll focus on the concept and the naming system.
BEM methodology encourages designers and developers to think of a website as a collection of reusable component blocks that can be mixed and matched to create interfaces. A block is simply a section of a document, such as a header, footer, or sidebar, illustrated below. Perhaps confusingly, “block” here refers to the segments of HTML that make up a page or application.
Blocks can contain other blocks. For example, a header block might also contain logo, navigation, and search form blocks, as seen below. A footer block might contain a site map block.
More granular than a block is an element. As the BEM documentation explains:
An element is a part of a block that performs a certain function. Elements are context-dependent: they only make sense in the context of the block they belong to.
A search form block, for example, contains a text input element and a submit button element, as evident below. (To clarify, we’re using “element” in the design element sense rather than the HTML element sense.)
A main content block, on the other hand, might have an article-list block. This article-list block might contain a series of article promo blocks. And each article promo block might contain image, excerpt, and “Read more” elements, as presented below.
Together, blocks and elements form the basis of the BEM naming convention. According to the rules of BEM:
- block names must be unique within a project
- element names must be unique within a block
- variations of a block—say, a search box with a dark background—should add a modifier to the class name
Block names and element names are usually separated by a double underscore (.block__element
). Block and element names are typically separated from modifier names by a double hyphen (for example, .block--modifier
or .block__element--modifier
).
Here’s what BEM looks like using a search form example:
<form class="search"> <div class="search__wrapper"> <label for="s" class="search__label">Search for: </label> <input type="text" id="s" class="search__input"> <button type="submit" class="search__submit">Search</button> </div></form>
A variation of this form with a dark background might use the following markup:
<form class="search search--inverse"> <div class="search__wrapper search__wrapper--inverse"> <label for="s" class="search__label search_label--inverse">Search for: </label> <input type="text" id="s" class="search__input search__input--inverse"> <button type="submit" class="search__submit search__submit--inverse">Search</button> </div></form>
Our CSS might look like this:
.search { color: #333;}.search--inverse { color: #fff; background: #333;}.search__submit { background: #333; border: 0; color: #fff; height: 2rem; display: inline-block;}.search__submit--inverse { color: #333; background: #ccc;}
In both our markup and CSS, search--inverse
and search__label--inverse
are additional class names. They’re not replacements for search
and search__label
. Class names are the only type of selector used in a BEM system. Child and descendant selectors may be used, but descendants should also be class names. Element and ID selectors are verboten. Enforcing block and element name uniqueness also prevents naming collisions, which can become a problem among teams.
There are several advantages to this approach:
- it’s easy for new team members to read the markup and CSS, and understand its behavior
- adding more developers increases team productivity
- consistent naming reduces the possibility of class-name collisions and side effects
- CSS is independent of markup
- CSS is highly reusable
There’s a lot more to BEM than can comfortably fit in a section of a chapter. The BEM site describes this methodology in much greater detail, and also features tools and tutorials to get you started. To learn more about the naming convention aspect of BEM, another fantastic resource is Get BEM.
Atomic CSS
If BEM is the industry darling, Atomic CSS is its rebellious maverick. Named and explained by Thierry Koblentz of Yahoo in his 2013 piece, “ Challenging CSS Best Practices,” Atomic CSS uses a tight library of class names. These class names are often abbreviated and divorced from the content they affect. In an Atomic CSS system, you can tell what the class name does—but there’s no relationship between class names (at least, not those used in the stylesheet) and content types.
Let’s illustrate with an example. Below is a set of rules in what we might call a conventional CSS architecture. These rulesets use class names that describe the content to which they apply—a global message box, and styles for “success,” “warning,” and “error” message boxes:
.msg { background-color: #a6d5fa; border: 2px solid #2196f3; border-radius: 10px; font-family: sans-serif; padding: 10px;}.msg-success { background-color: #aedbaf; border: 2px solid #4caf50;}.msg-warning { background-color: #ffe8a5; border-color: #ffc107;}.msg-error { background-color: #faaaa4; border-color: #f44336;}
To create an error message box, we’d need to add both the msg
and msg-error
class names to the element’s class
attribute:
<p class="msg msg-error">An error occurred.</p>
Let’s contrast this with an atomic system, where each declaration becomes its own class:
.bg-a { background-color: #a6d5fa;}.bg-b { background-color: #aedbaf;}.bg-c { background-color: #ffe8a5;}.bg-d { background-color: #faaaa4;}.bc-a{ border-color: #2196f3;}.bc-b { border-color: #4caf50;}.bc-c { border-color: #ffc107;}.bc-d { border-color: #f44336;}.br-1x { border-radius: 10px;}.bw-2x { border-width: 2px;}.bss { border-style: solid;}.sans { font-style: sans-serif;}.p-1x { padding: 10px;}
That’s a lot more CSS. Let’s now recreate our error message component. Using Atomic CSS, our markup becomes:
<p class="bw-2 bss p-1x sans br-1x bg-d bc-d"> An error occurred.</p>
Our markup is also more verbose. But what happens when we create a warning message component?
<p class="bw-2 bss p-1x sans br-1x bg-c bc-c"> Warning: The price for that item has changed.</p>
Two class names changed: bg-d
and bc-d
were replaced with bg-c
and bc-c
. We’ve reused five rulesets. Now, let’s create a button:
<button type="button" class="p-1x sans bg-a br-1x">Save</button>
Hey now! Here we’ve reused four rulesets and avoided adding any more rules to our stylesheet. In a robust atomic CSS architecture, adding a new HTML component such as an article sidebar won’t require adding more CSS (though, in reality, it might require adding a bit more).
Atomic CSS is a bit like using utility classes in your CSS, but taken to the extreme. Specifically, it:
- keeps CSS trim by creating highly granular, highly reusable styles, instead of a ruleset for every component
- greatly reduces specificity conflicts by using a system of low-specificity selectors
- allows for rapid HTML component development once the initial rulesets are defined
However, Atomic CSS is not without controversy.
The Case Against Atomic CSS
Atomic CSS runs counter to just about everything we’ve been taught on writing CSS. It feels almost as wrong as sticking style
attributes everywhere. Indeed, one of the major criticisms of the Atomic CSS methodology is that it blurs the line between content and presentation. If class="fl m-1x"
floats an element to the left and adds a 10px margin, what do we do when we no longer want that element to float left?
One answer, of course, is to remove the fl
class from our element. But now we’re changing HTML. The whole reason behind using CSS is so that markup is unaffected by presentation and vice versa. (We can also solve this problem by removing the .fl {float: left;}
rule from our stylesheet, although that would affect every element with a class name of fl
.) Still, updating the HTML may be a small price to pay for trimmer CSS.
In Koblentz’s original post, he used class names such as .M-10
for margin: 10px
and .P-10
for padding: 10px
. The problem with such a naming convention should be obvious. Changing to a margin of 5px or 20px means we’d need to update our CSS and our HTML, or risk having class names that fail to accurately describe their effect.
Using class names such as p-1x
, as done in this section, resolves that issue. The 1x
part of the class name indicates a ratio rather than a defined number of pixels. If the base padding is 5px (that is, .p-1x { padding: 5px; }
), then .p-2x
would set 10px of padding. Yes, that’s less descriptive of what the class name does, but it also means that we can change our CSS without updating our HTML, and without creating a misleading class name.
An atomic CSS architecture doesn’t prevent us from using class names that describe the content in our markup. You can still add .button-close
or .accordion-trigger
to your code. Such class names are actually preferable for JavaScript and DOM manipulation.
BEM versus Atomic CSS
BEM works best when you have a large number of developers building CSS and HTML modules in parallel. It helps to prevent the kind of mistakes and bugs that are created by sizable teams. It scales well, in part, because the naming convention is descriptive and predictable. BEM isn’t only for large teams, but it works really well for large teams.
Atomic CSS works better when there’s a small team or a single engineer responsible for developing a set of CSS rules, with full HTML components built by a larger team. With Atomic CSS, developers can just look at a style guide—or the CSS source—to determine which set of class names they’ll need for a particular module.
Know when to go your own way
In practice, your CSS may include a mix of approaches. You may have class names that describe content or components in addition to utility class names that affect layout. If you don't have full control over the markup, as with a CMS, then neither of these approaches may be useful. You may even need to use long and specific selectors to achieve what you want.
Conclusion
After reading this chapter, you should now know:
- how to organize your CSS for easier development and maintenance
- how browsers determine which CSS rules to use
- why class selectors are the most flexible selector for writing scalable, maintainable CSS
- the basics of BEM and Atomic CSS, and the pros and cons of each
In the next chapter, you’ll learn what to do when you notice a bug in your CSS. We’ll also discuss several tools for making your CSS files smaller.