CSS preprocessors have given us a handful of tools to re-architect our front-end code. We can keep things DRY with includes and extends or perhaps use nesting for code organization. All of these features allow a CSS rule to grow in complexity, but few people ever talk about managing that problem.
Let’s take a look at a modern CSS rule. We’ll talk about how they’re structured and why we do what we do. Perhaps this will help you organize your own CSS!
Here’s a very standard box. It is one of many box styles in our pretend design. It’s sole purpose is to feature and highlight content and it can live almost anywhere in our UI.
(Note, we use SCSS primarily so all our demo code will as well.)
It helps to think that rules tell a story. Our rule has a name. It has “parents” via @extend. It has a few properties and when you interact with it, it will do something (change its background color to green). When put in different situations via media queries or in
.sidebar, it will change its appearance.
Let’s step through some of the best practices we use to build
Similar to includes, imports, or inheritance in other languages, we handle dependencies first by putting every @extend at the top. This cuts down on debug time later as we always know where to look for dependency problems.
%center-box is a placeholder selector, which means its name does not get compiled in our output CSS. Instead, its properties appear wherever we call them.
@extend can be a tricky feature, as it can tinker with the ordering of our output CSS. All the more reason to put them early in our rule. Every bit of organization helps!
Utility or modifier classes like
.info-box come next for similar reasons. I’ll talk more about these in a moment.
Another advantage to putting external dependencies first is that it clarifies what the final output will be for a rule, as anything in a dependency would be overridden.
It’s not uncommon to have a base class with a few other classes that change the meaning of an element. You’ll see this a lot with buttons or boxes, especially in frameworks:
In this example,
.box is a base class that gives us some starting structure. We then inherit other styles through a series of modifier classes in an effort to nudge our box into place. This gets messy over time as we end up using a growing number of modifier classes to define what is essentially an unique element. This is where extends help again:
We can then use this to new rule to write a more semantic class attribute:
To get this effect without a CSS pre-processor we’d have to write really long rules. This quickly becomes difficult to manage:
It’s important to note that using extends or includes creates a dependency, so be careful! Also, as a rule of thumb, you should try to stick to one base class, one modifier, and one state:
That’s much cleaner!
Everyone seems to have a preferred order for properties. Some prefer alphabetical. Those who prefer grouping can almost never decide on the correct order. For example, I’m a huge fan of defining components starting with the outside first, then working inward:
- Where is the box? (position, z-index, margins/padding)
- What kind of a box is it? (display, box-sizing)
- How big is the box? (width, height)
- Box shadows, borders, etc
Intelligent grouping of properties will do a lot to ensure your CSS is accessible to other developers. Another common standard is “one rule per line.” As always, the most important guideline is be consistent.
Here’s where things get a little weird! You’ll notice that we include a mixin in our rule:
fadeIn() is a dependency. So why isn’t it at the top? Mixins perform operations and inject the resulting CSS into the rule. This means that they are often dependent upon property ordering defined within the same rule. Grouping them near related properties helps make sense of output CSS. In simpler cases, I recommend putting mixins right after basic properties but before sub-selectors, which we’ll discuss next.
Sub-Selectors (or “Think of the Children”)
The next section of
.featured-box deals with specific styles for its children. In our example above we’re targeting the
.title class specifically:
Notice that I didn’t use a H3 or any other headline for that matter. Doing so restricts
.featured-box to a specific HTML structure, which may not always be best for accessibility or accurate with regard to the hierarchy of content. For example,
.featured-box could be used in a sidebar with a H4, or at the top of the page with a big H2.
Complex objects could call for dozens of sub-selectors. When this happens, consider putting common styles in a base rule, and meatier objects (that may exist on their own) into their own rules. Expanding on our original example, let’s pretend we have a stylized list within
This is less ideal than:
In this way we aren’t losing the special case that is a
.tab-list inside of a
.featured-box amongst all the other styles.
State and Actions
CSS gives us a few actions to work with specific to our selected element. Most elements have :focus or :hover. Anchors have :visited and radio buttons have :checked. Often we shim these with classes or data attributes for use in richer interfaces:
Actions and state are usually dependent upon basic properties, but are often overridden depending on context. For example, the animation we may associate with
.is-sending may be different on desktop than on mobile, or we may have to override a color on hover of an anchor’s default :link state.
As a side note, notice that I am using
.is-sending instead of
.sending. I always append
.is- to state classes to more clearly convey what they are.
Context Specific Styles
Context related styles always come last because they often have radical changes to appearance and layout. This fits well with a mobile first strategy, as you can clearly denote the evolution of a rule as you gain screen space.
In our example above, we’re noting two different context changes:
.featured-boxis on a larger display.
In this scenario, we want
.featured-box to be full width and have a subtle appearance when in
.sidebar. On larger screens, we want it to be 50% of the available width and ensure that no matter what it is center aligned regardless of other styles. The ordering doesn’t matter here due to the specificity of the two changes.
.sidebar & will trump the media query change.
However, we put media queries last in rules because they most often deal with layout related changes, and putting those sort of changes in a predictable place in the same way we handled extends helps with debugging.
Per element media queries could alleviate some of the pain here, but at the time of this writing we do not have a clean CSS-only solution. Though many people have put together workable solutions. Things can get out of hand when you have to consider an element’s appearance not only across browsers and devices, but location in the UI and relation to other elements.
This just scratches the surface of what it takes to write clean, maintainable CSS architecture. In the wild, our
.featured-box rule would likely be more complicated, with more dependencies, basic properties, and context changes. What best practices does your team follow to help keep complexity at bay?