How to Scale and Maintain Legacy CSS with Sass and SMACSS

We’ve been big fans of SMACSS for a long time, however a pure SMACSS approach works best with plain old CSS on a brand new project. On our marketplace sites, we write style sheets in Sass and have several years’ worth of legacy CSS to deal with.

We’ve recently been refining the way we do CSS to make it easier for our growing team to maintain our style sheets without throwing away our existing front end & starting from scratch.

What we’ve ended up with is an approach loosely based on SMACSS that still solves the problems originally tackled by SMACSS & OOCSS, but with a few modifications and some ideas cherrypicked from places like BEM and Toadstool.

Note: You’ll need to be somewhat familiar with SMACSS for the rest of this post to make any sense. Here’s an overview.

Our take on SMACSS

SMACSS defines five categories of styles:

  1. Base
  2. Layout
  3. Module
  4. State
  5. Theme

Our approach looks more like this:

  1. Base
  2. Layout
  3. Module
  4. State
  5. Theme

Base

In newer projects our base styles are just Normalize plus some basic default element styles (colors, typography, margins & padding).

Unfortunately, the marketplace CSS comes from a time when aggressive CSS resets were cool, as well as having some unfortunate default typography styles that we pretty much always override. These styles are difficult to change without affecting all the other CSS that has been built on top of them over the years.

Even with those drawbacks, we can still treat our base styles just like SMACSS base styles.

Layout

In our approach everything that is not a base style or a global state class is a module.

SMACSS draws a distinction between major layout components (header, sidebar, grid, etc) and everything else. We’ve found this distinction unnecessary for a couple of reasons:

  • Modules often “lay out” their child components in the same way that major components are laid out on the page.
  • Even if we’re 100% certain a component will never be reused, there is no benefit in treating it any differently to reusable components.

The line between layouts and modules is too fuzzy for it to be worth keeping layouts around as a special category.

Module

Modules are standalone, reusable components that have no knowledge of their parent container. Their only dependencies are the app’s base styles. We can safely delete a module when it’s no longer needed without causing changes elsewhere in our CSS.

BEM double underscore syntax is used to scope child components to a module, and we use CSS child selectors liberally to minimise the depth of applicability.

BEM double hyphen syntax is used as a modifier to indicate subclasses or, when used in combination with the is- keyword, module-specific state classes.

Since setting a module’s width, position or margins would require knowledge of the context it appears in, our modules are always either full-width block elements or inline elements.

Here’s a simple example:

modules/_my_module.sass
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
.my-module
  background-color: maroon
  position: relative

  > a
    color: aqua

.my-module--important
  @extend .my-module
  border: 3px solid fuchsia

.my-module--is-active
  background-color: red

.my-module__close-button
  position: absolute
  right: 0
  top: 0

When I first started writing modules like this I’d often end up with huge modules with complex class names like .my-module__child-component__grandchild-component--modifier.

But aside from position and dimension properties, most child components can be extracted out into their own standalone modules. So if you leave the positioning up to the parent, we end up with 3 smaller, standalone modules.

modules/_my_module.sass
1
2
3
4
5
.my-module
  // ...

.my-module__child-component
  width: 100px
modules/_child_component.sass
1
2
3
4
5
6
.child-component
  // ...

.child-component__grandchild-component
  position: absolute
  top: 10px
modules/_grandchild-component.sass
1
2
3
4
5
.grandchild-component
  // ...

.grandchild-component--modifier
  // ...
example.html
1
2
3
4
5
6
7
8
9
10
11
12
13
<div class="my-module">

  <div class="my-module__child-component">
    <div class="child-component">

      <div class="child-component__grandchild-component">
        <div class="grandchild-component--modifier"></div>
      </div>

    </div>
  </div>

</div>

.grandchild-component and .child-component are now independent of their parent containers. It’s up to a module to position containers for its children. You do end up with a tiny bit more structural HTML, but with the benefit that nested UI components are now completely decoupled from each other.

State

Module-specific state classes are defined in the same file as the module itself (see .my-module--is-active above) but we keep global state classes seperate, e.g. .is-hidden.

Theme

We do “theme” each of the eight marketplaces (e.g. different color schemes on ThemeForest and GraphicRiver), but we achieve this by setting variables for site-specific styles in a config file.

Some Sass magic to bring it all together

Configuration

_config.sass is the very first file included in our main application.sass. In it, we assign to global variables all the common values we’ll use. This includes colors, font sizes & families, responsive breakpoints and more.

We also include a marketplace-specific config file to set variables for each marketplace’s color scheme.

Mixins

All of our mixins are kept in their own files in a mixins directory and are available globally. We also import a few vendor mixin libraries, like Compass which we use for spriting and vendor prefixing.

Grid

Even our grid framework is just a module.

We’re steering clear of using grid classes like .span-5 in HTML and instead using Susy to keep column-based widths in CSS alongside the modules they belong to.

modules/_page.sass
1
2
3
4
5
6
7
8
.page
  // ...

.page__sidebar
  @include span-columns(3, 12)

.page__content
  @include span-columns(9 omega, 12)

We wrap everything in a .grid module that’s just a Susy grid container.

Internet Explorer

We’re still supporting IE 7 & 8. We used to use the HTML5 Boilerplate method of targeting those browsers, but that meant we were sending lots of styles scoped to .lt-ie8 down to other browsers that would never use them.

Now we’re using this trick to generate standalone application-ie8.css and application-ie7.css style sheets for those browsers.

IE-specific styles (like everything else) live right inside the module they belong to:

modules/_my_module.sass
1
2
3
4
5
.my-module
  color: chartreuse
  @if $ie7
    position: relative
    zoom: 1

Good browsers are served a nice clean application.css without any IE junk, while old IE users get their own special version instead.

What we’re not doing

We’re not aiming for readable CSS output. When you write modules like this you can look at the class name and go straight to the correct Sass file instead of digging around in DevTools trying to figure out where a certain style is coming from. Also, Source Maps.

We’re not aiming to remove every last bit of duplication from our compiled CSS. We want to make development as easy as possible without impacting performance. So far including mixins in modules (and thus adding some duplicate styles) instead of @extending everything in sight hasn’t hurt us in terms of raw file size (gzip chews up most of the duplication - we’re sending about 39kb of CSS over the wire).

Conclusion

We started a few months ago by adding a single module file to our somewhat dirty style sheets directory. As we add new features & convert existing ones, our modules directory is steadily outgrowing what’s left of our old CSS.

The main application.sass file for ThemeForest now looks like this:

application.sass
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Config
@import config_global
@import config_themeforest

// Vendor mixin libraries
@import compass
@import susy

// Our mixins
@import mixins/**/*

// Old crud. Our base styles, plus everything else that will eventually 
// be converted into modules.
@import old_stuff/**/*

// Modules
@import modules/**/*

// Global state classes
@import state

We were successfully able to modernise our CSS architecture without throwing out our existing front end and starting from scratch.

Further reading

  • style is a reference starter project for this approach. Try it out and let us know what you think!
  • If you haven’t read SMACSS, most of it is free to read on smacss.com.
  • Harry Roberts has a whole series of great posts on scalable CSS. Start with this one on BEM syntax and work your way through the archives.
  • Nicolas Gallagher has also done a ton of work in this area. About HTML Semantics & Front End Architecture is a good overview of some of the thoughts that led to our approach.