Scalable CSS: Top tips for writing and maintaining CSS for scale | Heart Internet Blog – Focusing on all aspects of the web

Writing and maintaining CSS for a small site is very different from a large site or multiple sites. How exactly we developers can and should write CSS for scale varies according to the specifics of what we’re working on.

From my experience, what happens all too often is CSS files become an unstructured mess. Changes require new code, which gets tacked on at the end. Or somewhere in the middle. Or maybe somewhere else.

In this article, I’ll go through various methods I’ve used trying not to write too much CSS, while also generally keeping my sanity. Ideally, the methods presented here can be used to scale up your development practices in such a way that, even if you don’t need a certain method right now, you’ll have an idea what you might do to avoid losing control.

Defining scale

The “add new styles at the end” method might be widely used but it’s not going to help you past a certain point.

When you’ve built a site and deployed it, you’re free and can now bask in its glory. That basking will end when your team expands or the scope of your work changes.

Themes

Sales: “We promised a client they can have their own colours.”

You: 😱

Sales: “Here are the PowerPoint slides.”

You: 😱😱

Sales: “We’ve shown you which colours we want to change.”

You: 😱😱😱

Sales: “How hard can it be to set some colours?”

(Note: Everyone’s job looks easy if you don’t understand it.)

Sales: “We showed all the other clients the customisations.”

You: 😱

Sales: “They love it”

You: 😱😱

Sales: “There are ten of them”

You: 😱😱😱

Sales: “I think I should call an ambulance.”

Sales: “You seem to have passed out. Does that happen often? Should I send for help?”

(Note: Playing dead might work with bears but it’s less effective with sales staff.)

Large team

A 2015 poll on CSS Tricks showed 75 percent of CSS is edited by one or two people. As with any programming language, structure and consistency become more crucial the more people are involved. Even if you are only one person who edits a project, remembering what you wrote six months ago can be as much of a challenge as working with someone else.

Multiple architectures

Maintaining your styles across multiple architectures is as much a branding problem as a technical problem even if the impact is technical. How exactly do you keep your look and feel consistent when one team is using React and another Jekyll?

Ingredients

A good thing about structuring your CSS is knowing that you’re not alone and many others have gone ahead of you. A bad thing is the realisation that there are many varied solutions out there, which makes it hard to know what’s right for you.

Vanilla CSS

Plain old native CSS has structure by way of the various selectors it offers. Irrespective of what solution you choose, having a good understanding of CSS selectors will help make your life as a CSS developer significantly easier.

If they’re used well, you might not need to go any further.

In the modern CSS world, ID selectors are generally considered to be too specific and, as such, are best avoided. The key problem with ID selectors is they’re really difficult to override which makes scaling all the more difficult.

Class selectors can be pretty much anything, which is both good and bad. Without a clear naming scheme, classes can quickly devolve into chaos.

Attribute selectors can also be pretty much anything but they do carry great semantics with them. The semantic meaning of attributes makes them extremely powerful when used carefully.

Finally, element selectors are broadly useful but can also be too specific. If you set margins on a simple p, then you’ll need to override that p over and over.

Every selector in CSS is defined in the global scope. That is, each selector is available to the entire rendered document at once.

This core feature of CSS is often seen as a bug and several attempts have been made to “correct” it. Heydon Pickering raised a storm discussing such CSS features in 2016 (CSS Inheritance, The Cascade And Global Scope: Your New Old Worst Best Friends).

SMACSS

With the name Scalable and Modular Architecture for CSS, SMACSS seems to offer everything we want to cope with scale. It’s scalable! It’s modular! Perfect!

SMACSS takes us beyond using one file for everything by splitting CSS files into a logical order. This improves maintainability even for small projects.

While SMACSS lists five categories of splitting CSS (base, layout, module, state and theme), I’ve taken to using a modified structure: core, blocks, components, and site, which are detailed in the File Structures section below.

BEM

When I first looked into the Block Element Modifier naming method, I found it very confusing. The broad concept seems clear enough but specifics of implementation are lacking. I assume this is somewhat intentional as BEM provides a loose structure that can be used however you like.

The key feature of BEM is to apply a structured definition to classes. Once you wrap your head around it, it makes writing components highly beneficial.

Beyond standard BEM syntax, Harry Roberts describes adding namespacing to provide additional scope to the naming structure under the name BEMIT.

Atomic Design

What does a design system have to do with CSS? Atomic Design provides a way of structuring designs into separate interrelated parts. The methodology makes as much sense when applied to user interface code and hence CSS.

The key advantage to scalable CSS is in providing an easy-to-understand structure, which helps immensely when working in dispersed teams.

Sass

I’m generally reluctant to go near variations over core language features in part to reduce dependencies. It took me a long time to adopt jQuery, and I’m reluctant to go near TypeScript.

In similar ways, it took considerable thought and consideration before I adopted a CSS pre-processor, in this case Sass. The key features of variables and nesting were enough in the end to convince me, especially given Sass compiles to vanilla CSS.

Even having adopted Sass, I remain reluctant to push too deep into the framework to make it easier to drop it whenever the time comes.

Left on the shelf

Not every design pattern scales well. Here are the methodologies I’ve looked at but haven’t yet found a use for.

Atomic CSS

Atomic CSS uses functional classes to control scope. However, it essentially reverts back to compact inline styles. If you want to learn more about Atomic CSS, John Polacek wrote a great summary of the methodology, “Let’s Define Exactly What Atomic CSS is“.

While there are certainly proponents of Atomic CSS out there, I’m not one of them. I find Atomic CSS to be abstracted to the point of nonsense and overrides don’t really make sense.

If you need to set a blue background, the class is easy enough for one project.

.bgr-blue {
  Background: #00f;
}

The problem comes in as soon as you need to change that background in a subsequent project. You’re left with the difficult choice of either repurposing .bgr-blue and set another colour in the CSS or modifying your HTML.

CSS in JS

The first line on the JSS site declares, “JSS is a more powerful abstraction over CSS”, which immediately prompts questions such as, “How exactly does JavaScript improve CSS?” and “Is CSS broken?“. I’m in fair agreement with both Bruce Lawson above and Kevin Ball’s article “CSS in JS is like replacing a broken screwdriver with your favorite hammer“. JSS has never particularly made sense to me. It’s as if we never learned from JSSS.

CSS Modules

The global scope of CSS can become a problem when CSS scales and CSS Modules set out to ease exactly that by providing automated prefixing.

CSS Modules may provide everything you need to scale CSS. Each module contains vanilla CSS (or whatever your like), which is automatically scoped by the framework. Sheer brilliance.

Should you ever decide to shift away from CSS Modules, the code in each module is ready to go. You would have to manually scope but that’s it — no need for a significant rewrite.

For widely distributed CSS libraries, platform neutrality is essential, and it’s at this point that CSS Modules become difficult to implement. While your team may be at the cutting edge of web methodologies, the industry has a long tail. What happens when you inherit a project built with an unsupported framework? Unfortunately, the answer is you don’t use CSS Modules.

CSS Blocks

Relatively new in this space, CSS Blocks aim a step further beyond the automated scoping of CSS Modules and offer build-time error catching, dead code elimination and static analysis amongst other features. This requires a very tight integration between HTML, JavaScript and CSS, which is great if you have a tight control over your environments.

Stylable

Once again fixing the problem (or otherwise) of scope in CSS, Stylable also has tight programmatic integration with JavaScript and HTML.

The rise of CSS Modules, CSS Blocks and Stylable is somewhat reminiscent of the early days of JavaScript frameworks when MooTools, jQuery and YUI all offered variations of the same thing. While much of the power of those frameworks has been integrated into the JavaScript language and browser DOM itself, it will be interesting to see what impact these tools will have on CSS itself.

The recipe for scalable CSS

Putting it all together involves a combination of solutions. Not every technique is required for every project and, if you set the right patterns early, you will be able to add complexity as and when you need to.

You can see a working version of everything here on GitHub.

Code integrity

Before you get to writing any code, take the time to set up a linting tool such as Stylelint, CSSLint or Sass Lint if you’re using a pre-processor. Linters ensure consistent syntax across your project and can catch errors before you get too far. It doesn’t really matter what rules you set as long as you follow them.

Naming

Finding names for things is notoriously difficult. However, the effort spent to get there is well worth it.

Component naming

Sparing use of BEM syntax provides a structure to a project’s components. Rather than applying BEM naming to every element, I find it much easier to only name those elements that require it in order to scope the subsequent styles.

Simple components may only need blocks to be named:

.nav { /* navigation component */
  …

  > ul { /* list directly under the nav */
    …

    > li { /* list item directly under the list */
      …
    }
  }

  a { /* all anchors inside the navigation block */
    …
  }
}

More complex components can add BEM naming as and when required:

.tab { /* tab component */
  &__tab-list { /* tab list */
    …
  }

  &__tab { /* tab item */
    …

    a { /* anchor inside a tab item */
      …
    }
  }
}

In effect, this is a BEM-light implementation, which combines the strength of the pattern with the flexibility of vanilla CSS.

Namespacing

Namespacing is used as a way of scoping in programming languages, and there’s nothing stopping us from using the method in CSS. I favour a broad three-letter namespacing method to clearly show the origin of each class name.

Namespacing may not be required if you only work on one project. With pre-processed nesting, it’s easy enough to add at a later date should you find it necessary.

For example, in the primary project, the namespace will be pri-:

/* without namespacing */
.tab {
  …
}

/* with namespacing */
.pri-tab {
  …
}

Blocks unique to an inheriting project use their own namespace:

/* secondary project */
.sec-nav {
  …
}

When inspecting the corresponding HTML, it’s immediately clear where the source can be found. Depending on the level of customisation, the odds are you won’t need to use a secondary namespace very often.

File structures

Rather than one big file or several big files, there’s a lot of benefit in splitting your CSS into logical folders. The original inspiration for this came from SMACSS. However, I’ve adapted the folders and structure over several years of practical use.

Depending on the complexity of the project, it may help to version each file. Implementing Semantic Versioning allows you to make breaking changes on an individual file with ease. If you don’t think you need to reach that point yet, it’s very easy to append versions to the file names when you do.

Core

The Core folder is home to the basics for your CSS library: global styles, utilities, typography and variables. Anything that you define once and use frequently belongs here. The content of this folder isn’t expected to change much once it’s written.

While CSS resets have been popular in the past, the commonality of modern browsers means you should be fine without one. Be brave and move on from that reset!

Shift all your break points, font faces and colour palettes into a variables file. I also set a global measure, on which many sizes such as grid-gap, padding and margins are derived and place it here.

Base typographic sizing also lives here (see A More Modern Scale for Web Typography for a great reference to get you going) along with any basic structures such as a common grid.

Blocks

Home to broad constructs from which the site is built, such as header, content, and footer. Each of these is essentially an organism following the Atomic Design methodology and, from a CSS perspective, the organisms don’t need to know what components do underneath them limiting the amount of CSS required in each file.

Components

Each encapsulated component is contained in the Components folder. You could potentially replace this with Atomic Design’s molecules and organisms or similar depending on the complexity you have.

All

Styles that aren’t shared are in the all file first, inheriting everything from core, blocks, and components.

For simple themed sites, a new all.css for the inheriting project will be enough with each override (typically colour) contained in there.

More complex themes may require a deeper level of infrastructure, mirroring the base project as required.

tertiary.css
/tertiary/buttons.css
/tertiary/footer.css
/tertiary/header.css
/tertiary/nav.css

Notice the structure doesn’t completely reflect the original project. Only the complexity required has been added.

Ye Olde Browsers

It’s tempting to add legacy browser code inline or in a separate file. However, a better approach is to add the code to the base of the relevant file with as many comments as you can.

.pri-grid {
  …
}

/*
No-flex
Support for non-flexbox browsers. Remove this once IE10 support is no longer required.
Minimum width fixed to 900px to make things easier
*/
.no-flexbox {
  .pri-grid {
    …
  }
}

While the scoped class above is derived from Modernizr, feature queries (@supports) are a native browser method of feature detection to achieve the same concept.

Theming

When variables in CSS first became possible with the likes of SCSS, my first thought was to use them for themes.

button {
  color: $main-colour;
  background: $secondary-colour;
  border-color: $main-colour;    
}

This is great until $secondary-color clashes with $main-colour when you apply a different palette.

Breaking my rule of investing too heavily in pre-processing variants, I use Sass maps and functions to provide structure to colour palettes and make them easier to use.

Defining a palette for each project and calling them with a function ensures that only colours from your colour palette are used ensuring strict brand adhesion and love from designers.

/* colour map unique to this theme */
$pri-palette: (
  neutral: ( /* neutral palette */
    pure-white: #fff,
    mid-grey: #808080,
    pure-black: #000,
  ),
  accent: ( /* accent palette */
    mint: #28765c,
    corn: #b1ab3c,
  )
)

/* Return colour value from given palette map */
@function colour($palette, $group, $name) {
  $colour: map-get(map-get($palette, $group), $name);

  // if the colour isn't found in the provided palette, throw an error
  @if $colour == null {
    @error "Colour #{$name} not found in this palette #{$group}.";
  }

  @return $colour;
}

/* Project-specific implemetation */
@function pri-colour($group, $name) {
  @return colour($pri-palette, $group, $name);
}

While that’s somewhat complicated to set up, implementing the colours is easy.

.pri-button {
  /* theme, palette and colour all in one go */
  background: pri-colour(neutral, mid-grey);
  color: pri-colour(neutral, pure-white);
  border-color: pri-colour(neutral, pure-black);
}

When overriding these declarations, it’s immediately clear what has been themed.

.pri-button {
  background-color: sec-colour(primary, dark-cool-blue);
  color: pri-colour(neutral, pure-white);
  border-color: pri-colour(neutral, pure-black);
}

Levelling up

To take your CSS library to the next level, give it a name, create a separate versioned source repository, and add an npm package. If you don’t want to open your code to the public, a private package will work just as well.

Your library will take a life of its own when it exists as its own project.

To infinity and beyond

As scale adds complexity to a project, so also does it have a variety of potential solutions.

The methods described here have been hammered together over many projects, teams, and years. I recommend that, rather than adopt everything here as is, that you instead choose what works for your project(s), then scale up or down as you need.

Comments

Please remember that all comments are moderated and any links you paste in your comment will remain as plain text. If your comment looks like spam it will be deleted. We're looking forward to answering your questions and hearing your comments and opinions!

Got a question? Explore our Support Database. Start a live chat*.
Or log in to raise a ticket for support.
*Please note: you will need to accept cookies to see and use our live chat service