The End of Global CSS

CSS selectors all exist within the same global scope.

Anyone who has worked with CSS long enough has had to come to terms with its aggressively global nature — a model clearly designed in the age of documents, now struggling to offer a sane working environment for today’s modern web applications.

Every selector has the potential to have unintended side effects by targeting unwanted elements or clashing with other selectors. More surprisingly, our selectors may even lose out in the global specificity war, ultimately having little or no effect on the page at all.

Any time we make a change to a CSS file, we need to carefully consider the global environment in which our styles will sit. No other front end technology requires so much discipline just to keep the code at a minimum level of maintainability.

But it doesn’t have to be this way.

It’s time to leave the era of global style sheets behind.

It’s time for local CSS.


In other languages, it’s accepted that modifying the global
environment is something to be done rarely, if ever.

In the JavaScript community, thanks to tools like Browserify, Webpack and JSPM, it’s now expected that our code will consist of small modules, each encapsulating their explicit dependencies, exporting a minimal API.

Yet, somehow, CSS still seems to be getting a free pass.

Many of us — myself included, until recently — have been working with CSS so long that we don’t see the lack of local scope as a problem that we can solve without significant help from browser vendors. Even then, we’d still need to wait for the majority of our users to be using a browser with proper Shadow DOM support.

We’ve worked around the issues of global scope with a series of naming conventions like OOCSS, SMACSS, BEM and SUIT, each providing a way for us to avoid naming collisions and emulate sane scoping rules.

While undoubtedly a massive step forward for taming CSS, none of these methodologies have addressed the real problem with our style sheets. No matter which convention we choose, we’re still stuck with global selectors.

But all of that changed on April 22, 2015.


As we covered in an earlier post — “Block, Element, Modifying Your JavaScript Components” — we can leverage Webpack to import our CSS from within a JavaScript module. If this sounds unfamiliar to you, it’s probably a good idea to go read that article now, lest you miss the importance of what’s to follow.

Using Webpack’s css-loader, importing a component’s CSS looks like this:

require('./MyComponent.css');

At first glance — even ignoring the fact that we’re importing CSS rather than JavaScript — this is quite strange.

Typically, a require call should provide something to the local scope. If it doesn’t, it’s a sure sign that a global side effect has been introduced — often a symptom of poor design.

But this is CSS — global side effects are a necessary evil.

Or so we thought.


On April 22, 2015, Tobias Koppers — the ever tireless author of Webpack — committed the first iteration of a new feature to css-loader, at the time called placeholders, now known as local scope.

This feature allows us to export class names from our CSS into the consuming JavaScript code.

In short, instead of writing this:

require('./MyComponent.css');

We write this:

import styles from './MyComponent.css';

So, in this example, what does styles evaluate to?

To see what is exported from our CSS, let’s take a look at an example of what our style sheet might look like.

:local(.foo) {
color: red;
}
:local(.bar) {
color: blue;
}

In this case, we’ve used css-loader’s custom :local(.identifier) syntax to export two identifiers — foo and bar.

These identifiers map to class strings that we can use in our JavaScript file. For example, when using React:

import styles from './MyComponent.css';
import React, { Component } from 'react';
export default class MyComponent extends Component {
  render() {
return (
<div>
<div className={styles.foo}>Foo</div>
<div className={styles.bar}>Bar</div>
</div>
);
}
}

Importantly, these identifiers map to class strings that are guaranteed to be unique in a global context.

We no longer need to add lengthy prefixes to all of our selectors to simulate scoping. More components could define their own foo and bar identifiers which — unlike the traditional global selector model—wouldn’t produce any naming collisions.


It’s critical to recognise the massive shift that’s occurring here.

We can now make changes to our CSS with confidence that we’re not accidentally affecting elements elsewhere in the page. We’ve introduced a sane scoping model to our CSS.

The benefits of global CSS — style re-use between components via utility classes, etc. — are still achievable with this model. The key difference is that, just like when we work in other technologies, we need to explicitly import the classes that we depend on. Our code can’t make many, if any, assumptions about the global environment.

Writing maintainable CSS is now encouraged, not by careful adherence to a naming convention, but by style encapsulation during development.

As a result of this scoping model, we’ve handed control of the actual class names over to Webpack. Luckily, this is something that we can configure.

By default, css-loader transforms our identifiers into hashes.

For example, this:

:local(.foo) { … }

Is compiled into this:

._1rJwx92-gmbvaLiDdzgXiJ { … }

In development, this isn’t terribly helpful for debugging purposes. To make the classes more useful, we can configure the class format in our Webpack config as a parameter to css-loader:

loaders: [
...
{
test: /\.css$/,
loader: 'css?localIdentName=[name]__[local]___[hash:base64:5]'
}
]

In this case, our foo class identifier from earlier would compile into this:

.MyComponent__foo___1rJwx { … }

We can now clearly see the name of the identifier, as well as the component that it came from.

Using the node_env environment variable, we can configure different class patterns for development and production.

loader: 'css?localIdentName=' + (
process.env.NODE_ENV === 'development' ?
'[name]__[local]___[hash:base64:5]
' :
'[hash:base64:5]'
)
Now that Webpack has control of our class names, we can trivially
add support for minified classes in production.

As soon as we discovered this feature, we didn’t hesitate to localise the styles in our most recent project. We were already scoping our CSS to each component with BEM — if only by convention — so it was a natural fit.

Interestingly, a pattern quickly emerged. Most of our CSS files contained nothing but local identifiers:

:local(.backdrop) { … }
:local(.root_isCollapsed .backdrop) { … }
:local(.field) { … }
:local(.field):focus { … }
etc…

Global selectors were only required in a few places in the application. This instinctively led towards a very important question.

What if — instead of requiring a special syntax — our selectors were local by default, and global selectors were the opt-in exception?

What if we could write this instead?

.backdrop { … }
.root_isCollapsed .backdrop { … }
.field { … }
.field:focus { … }

While these selectors would normally be too vague, transforming them into css-loader’s local scope format would eliminate this issue and ensure they remain scoped to the module in which they were used.

For those few cases where we couldn’t avoid global styles, we could explicitly mark them with a special :global syntax.

For example, when styling the un-scoped classes generated by ReactCSSTransitionGroup:

.panel :global .transition-active-enter { … }

In this case, we’re not just scoping the local panel identifier to our module — we’re also styling a global class that is outside of our control.


Once we started investigating how me might implement this local-by-default class syntax, we realised that it wouldn’t be too difficult.

To achieve this, we leveraged PostCSS — a fantastic tool that allows you to write custom CSS transformers as plugins. One of the most popular CSS build tools today — Autoprefixer — is actually a PostCSS plugin that doubles as a standalone tool.

To formalise the usage of local CSS, I’ve open sourced a highly
experimental plugin for PostCSS called
postcss-local-scope. It’s still under heavy development, so use it in production at your own risk.

If you’re using Webpack, it’s a relatively straightforward process to hook postcss-loader and postcss-local-scope up to your CSS build process. Rather than document it here, I’ve created an example repository — postcss-local-scope-example—that shows a small working example.


Excitingly, introducing local scope is really just the beginning.

Letting the build tool handle the generation of class names has some potentially huge implications. In the long term, we could decide to stop being human compilers and let the computer optimise the output.

In the future, we could start generating shared classes between components automatically, treating style re-use as an optimisation at compile time.

Once you’ve tried working with local CSS, there’s really no going back. Experiencing true local scope in our style sheets — in a way that works across all browsers— is not something to be easily ignored.

Introducing local scope has had a significant ripple effect on how we approach our CSS. Naming conventions, patterns of re-use, and the potential extraction of styles into separate packages are all directly affected by this shift, and we’re only at the beginning of this new era of local CSS.

Understanding the ramifications of this shift is something that we’re still working through. With your valuable input and experimentation, I’m hoping that this is a conversation we can have together as a larger community.

To get involved, make sure you see it with your own eyes by
checking out postcss-local-scope-example.

Once you’ve seen it in action, I think you’ll agree that it’s not just hyperbole — the days of global CSS are coming to an end. The future of CSS is local.


Note: Automatically optimising style re-use between components would be an amazing step forward, but it definitely requires help from people a lot smarter than me. Hopefully, that’s where you come in ☺


Addendum

24 May, 2015: The original ideas presented in postcss-local-scope have been accepted into Webpack by Tobias Koppers, meaning that the project is now deprecated. Support for CSS Modules — as they are tentatively known — are now available in css-loader via an opt-in module flag. I’ve created a working example of CSS Modules in css-loader to demonstrate their usage, including class inheritance to intelligently share common styles between components.


Image Credit: Jay Mantri (http://jaymantri.com/post/112158023323)

One clap, two clap, three clap, forty?

By clapping more or less, you can signal to us which stories really stand out.