Written by Harry Roberts on CSS Wizardry.
N.B. All code can now be licensed under the permissive MIT license. Read more about licensing CSS Wizardry code samples…
One thing you will no doubt be familiar with, as a web developer, is the idea of scope. Wikipedia’s introduction to the subject:
In computer programming, a scope is the context within a computer program in which an identifier is in effect, and can be resolved to an entity – when the entity is visible.
A super simple example of scope in JavaScript:
var foo = "foo";
function myFunction() {
var bar = "bar";
}
console.log(foo);
console.log(bar);
Running this, and keeping an eye on your console, you should see two lines:
foo
Uncaught ReferenceError: bar is not defined
The first line reads foo. This is because foo
is defined in
global scope and is later logged in global scope. bar
is also logged in
global scope, but is defined in ‘local’ scope, inside myFunction()
, which is
why the second line reads Uncaught ReferenceError: bar is not defined
. We
don’t have access to the bar
variable because it exists in a different scope.
Scope basically deals with what is visible to a program based on the current scope, and the scope of the thing the program is trying to interact with.
Now, I am far from a programmer. I have a theoretical understanding of scope, but most, if not all practical, real life knowledge is nonexistent. Please forgive me if my details are a little rough around the edges.
Global scope is typically a bad thing in programming. Global scope means defining and using variables with no sandboxing; variables in global scope are unpredictable because:
Let’s take a simple PHP-based example. Let’s imagine you have a variable in
global scope called $name
:
$name = <holds the name of the logged in user>;
We can use this to display a message to the user on the homepage:
<h1>Hi there, <?php echo $name; ?>!</h1>
This is all very simple, and it works a treat, however, what happens later in
our program when we want to list the name of the user’s friends and, forgetting
about our previous definition, we redefine $name
again?
$name = <holds the name of the logged in user’s friend>;
$name
—which used to hold the logged in user’s name—now holds completely
different data. We have lost the old variable and reassigned it because we were
dealing with the two in the same scope.
Of course, no one would ever write any code like this, even someone like me can see that it’s an accident waiting to happen! Yet, we seem to have no problems with writing CSS this way…
Loose selectors are something I have written about before, but it is recently that I have noticed a parallel that can be drawn between them and scope in programming. Please do note that this is only a metaphor. CSS does not have scope like programming languages do; I am merely trying to use scope as an analogy for helping explain the impact and danger of loose selectors.
A loose selector in CSS might be very much like the $name
we used previously;
whilst this is accurate, it is not explicit or sandboxed enough. We might
initially have .name{}
to style up a form field. Then, six months later,
another developer might come along and need to style up a user’s profile page,
where he wraps the user’s name in a heading with a class of, you guessed it,
.name{}
!
The problem we have here is that styling meant for the user’s name will leak into the form field styling and, conversely, the styling on the form field will pick up styling meant for the user’s name.
A lot of the time, a loose selector is just one which is poorly named, so it’s not always necessarily about scope, but coupling better naming with the idea of at least quasi-scoping CSS can solve a lot of our problems.
CSS doesn’t have scope in quite the same way programming languages do, but let’s take a look at the following:
/**
* ‘Global’ scope.
*/
.some-widget{
}
.title{
}
Here we can see we have a component called .some-widget{}
and a class of
.title{}
. Some widget is a kind of scope unto itself; it’s a discrete chunk
of CSS that styles everything to do with .some-widget{}
. In terms of scope,
this is certainly a start.
With .some-widget{}
we also have a class of .title{}
, a very loosely named
class which exists in a quasi global scope. .title{}
is meant to be part of
.some-widget{}
but it completely lacks any scope at all and is very loose,
much like our .name{}
example. We could quite accidentally reassign or
accidentally reuse this class now because it exists in a quasi global scope;
what’s to stop another developer using this .title{}
class on a
‘Mr/Mrs/Ms’-style form input six months down the line?
This is CSS’ equivalent of global scope.
We could, of course, give our classes definite scope by nesting them! If we were to look at a piece of Sass we can actually see how the scoping might work, because it looks almost programmatic:
/**
* ‘Local’ scope, best illustrated with Sass.
*/
.some-widget{
.title{
}
}
Looking at that it is really easy to see a nice, explicit scoping; a .title{}
inside of .some-widget{}
. Now any styles on this titling element will only
apply if it exists in the scope of the widget! Perfect… or is it?
Unfortunately, for all this Sass gives us the scope we were after, it compiles out to the following, which is overly specific:
.some-widget{
}
.some-widget .title{
}
I have written a few times about the importance paying attention to your CSS selectors, and nesting is one of the easiest ways to fly in the face of that.
The main, and most fundamental problem with nested selectors is that they unnecessarily increase specificity, and specificity is a bad thing.
So, even though nesting gives us rock solid scope (and about as close to actual scope in CSS as you’ll ever get), it’s not the right answer. So what is?
The best way to handle ‘scope’ in CSS is with a quasi, implied scope, and the best way to achieve this, in my opinion, is BEM.
/**
* ‘Implied’ local scope, using BEM.
*/
.some-widget{
}
.some-widget__title{
}
BEM is a naming convention that
I have written about previously,
so you can familiarise yourself with it there. The way BEM helps us with scope
is to imply it by namespacing your selectors with the scope in which they
function. A class of .bar{}
operating in the scope of .foo{}
would now be
.foo__bar{}
.
This now means our loose .name{}
example becomes a nicely scoped
.profile__name{}
or .form__name{}
. We have a very detailed class which would
be nigh on impossible to reuse or reassign!
Not everything in CSS needs a scope, some things do need to exist globally.
Your .left{}
helper class, for example, does not have a scope inside of
something else.
However, .left{}
is still a fairly loosely named class. Although it doesn’t
necessarily require scope as such, it might be better named .float--left{}
.
This removes any ambiguity and decreases the chance of someone marking up, say,
map directions using a class of .left{}
. This is another similarity between
CSS and programming; the need to name things.
Naming things is very difficult,
but we need to get better at it.
Write longer classes; instead of a class like .logo{}
, opt for .site-logo{}
.
Make your classes a lot less loose by naming them a lot more specifically.
It might not always be a case of scoping, it might just be a case of picking a
better name for something with global scope.
Make sure any classes you write aren’t loose; make sure they’re always well named, and scoped if they need to be. Nesting selectors is a bad way of scoping, so use a naming convention like BEM to provide a quasi scoping.
Global scope and poorly named variables are absolute programming basics; there is no reason for our CSS to have the same unpredictable and loose traits that developers spend so much time avoiding.
N.B. All code can now be licensed under the permissive MIT license. Read more about licensing CSS Wizardry code samples…
Harry Roberts is an independent consultant web performance engineer. He helps companies of all shapes and sizes find and fix site speed issues.
Hi there, I’m Harry Roberts. I am an award-winning Consultant Web Performance Engineer, designer, developer, writer, and speaker from the UK. I write, Tweet, speak, and share code about measuring and improving site-speed. You should hire me.
You can now find me on Mastodon.
I help teams achieve class-leading web performance, providing consultancy, guidance, and hands-on expertise.
I specialise in tackling complex, large-scale projects where speed, scalability, and reliability are critical to success.