Better CSS

Lesson 2: Scope

One of the coolest and also most frus­trat­ing things about CSS is that any style has the scope to change any aspect of any ele­ment on a page (giv­en it has suf­fi­cient spe­cificity). We say that they have glob­al scope.

In this les­son we’re going to cover:

  1. An explan­a­tion of how scope ties into CSS’s cascade
  2. The bene­fits that scope affords us.
  3. Some of the draw­backs it brings.
  4. Tips for avoid­ing scope catastrophe.
  5. A brief look at how CSS Mod­ules can help us.

The Cas­cade #

First, to under­stand why all CSS rules have glob­al scope, we have to prop­erly under­stand CSS’s cas­cade algorithm. It’s an abso­lutely fun­da­ment­al part of CSS (the C stands for Cas­cad­ing!), but what does​“cas­cad­ing” actu­ally mean in this context?

The Cas­cade” is how browsers determ­ine which styles to apply to a page. As stylesheets are down­loaded, the browser reads each one, top to bot­tom, and applies visu­al changes to ele­ments tar­geted by the indi­vidu­al rules.

Stylesheets are broken down into three main categories:

  1. User-agent stylesheets — These are your browser’s default styles. The CSS spe­cific­a­tion isn’t strict on what these should be, so defaults vary quite a lot between dif­fer­ent browsers.
  2. Author stylesheets — The stylesheets pro­duced by the build­ers of a site. Most of the styles that users on the web see will fall into this cat­egory. They’re what make 99% of pages look the way they do.
  3. User stylesheets — Cus­tom stylesheets that a user can install to tail­or the appear­ance of web pages.

User-agent styles put forth sens­ible defaults, which are over­rid­den by author stylesheets, which in turn can be over­rid­den by user stylesheets. This is the cas­cade in a nut shell. Sev­er­al baseline style sets, which are over­rid­den by increas­ingly spe­cif­ic styles (as touched on in Les­son 1).

Without glob­al scope, CSS wouldn’t be able to achieve this beha­viour. If we were able to exclude cer­tain declar­a­tions from being over­rid­den, it would pre­vent user stylesheets tak­ing pre­ced­ence over user-agent and author styles.

Advant­ages of glob­al scope #

User stylesheets provide loads of access­ib­il­ity bene­fits. Those of us with visu­al impair­ments can inject cus­tom stylesheets to make the web more read­able. Be that through lar­ger font sizes, increased col­our con­trasts or col­our-blind-friendly palettes. Pretty cool! Without them and glob­al scope, we’d be exclud­ing mil­lions of people from visu­ally exper­i­en­cing the web.

In addi­tion to the visu­ally impaired, glob­al scope makes CSS really access­ible to those inter­ested in learn­ing web devel­op­ment. You don’t have to learn any com­plic­ated API, you just open up your browser developer tools and start tinker­ing. You can change any­thing! Such power! Such res­ults! This instant, low-bar­ri­er feed­back loop of CSS can really cata­pult those curi­ous enough into web development.

Dis­ad­vant­ages of glob­al scope #

For reas­on­­ably-sized sites that require a team of engin­eers to main­tain, glob­al scope is more often than not a recipe for dis­aster. Almost all front-end engin­eers at one point in their careers will add/​update/​remove a style, only to find it acci­dently leaks into some oth­er sec­tion of anoth­er page.

Example #

See the Pen  BaN­Zzog by Joe For­shaw (@joeforshaw) on Code­Pen.

Take the above example for instance. We’ve taken reas­on­able steps to scope our styles to just ele­ments that have a .blog-heading class. But that doesn’t stop anoth­er engin­eer bull­doz­ing their styles into our page. The high-spe­­ci­ficity, wide-scoped rule has overid­den the inten­ded look of our headings. 😑

Mod­u­lar­ity #

Glob­al scope makes any concept of mod­u­lar­ity in CSS very dif­fi­cult. Every declar­a­tion has the poten­tial to affect any ele­ment it likes, so it’s impossible to com­pletely pro­tect your UI from modi­fic­a­tion, be it delib­er­ate or acci­dent­al. Tomor­row anoth­er engin­eer could stick in a few !important styles and scrap your nicely-encap­su­lated, reusable widget.

If you told any back-end engin­eer that they had to use a pro­gram­ming lan­guage that gave all vari­ables glob­al scope, made every object’s intern­al state vis­ible, allowed any oth­er engin­eer to over­ride their code, they’d prob­ably have a few choice words for you. But that’s the dif­fi­cult real­ity of CSS devel­op­ment, everything’s up for grabs.

Tips for man­aging scope #

So we’ve talked a bit about why glob­al scope is import­ant for a lot of web users and also how it’s a pain for engin­eer­ing teams. But we need to find a nice middle-ground that helps both parties.

On the users’ side, we have browsers and the CSS spe­cific­a­tion that ensure user stylesheets will always have the scope to over­ride exist­ing styles. On the engin­eer­ing side, we have us, the style-smiths. It’s up to us to fig­ure out strategies for man­aging glob­al scope in a main­tain­able way.

As rules can touch any­thing they like, the best we can hope for is to avoid writ­ing styles that acci­dent­ally leak into oth­er parts of our UI. This takes dili­gence and team dis­cip­line; if just one engin­eer isn’t pulling in the right dir­ec­tion it’ll res­ult in a mess.

Whatever the solu­tion, to man­age scope effect­ively, we need to adhere to the fol­low­ing gen­er­al principle:

Keep scope lim­ited and deliberate

We have to lim­it the scope of our changes to just the ele­ments we delib­er­ately want to change. New styles should only affect the things we want and noth­ing more. No unin­ten­ded side-effects, no head­aches. So how do we do this?

Tip 1: Sep­ar­ate stylesheets for each page? 🤔 #

One pos­sible solu­tion that feels right ini­tially is to have sep­ar­ate spread­sheets for each page and only include the styles we need and noth­ing more. That way there’s less chance of styles writ­ten for a spe­cif­ic page leak­ing out­side to oth­er pages.

How­ever in real­ity, under­stand­ing which styles a page requires, and which styles it doesn’t, is a very hard task, espe­cially if you’re a big team. It requires every­one to hold in their head every pos­sible UI ele­ment that could pos­sibly appear on a page so that they can load in the right styles. That’s a really unscal­able situ­ation, so I wouldn’t recom­mend it for man­aging scope.

Often when stylesheets grow so large that they start to have notice­able browser per­form­ance issues, prun­ing out unused styles is a sens­ible decision. But this is rarely done at a page-by-page level, more often it’s per sec­tion, due to the chal­lenges described above. There­fore, using it as a meth­od for lim­it­ing scope won’t be effect­ive as leaks can still run rampant with­in those sections.

Tip 2: Avoid type select­ors #

Rule­sets that use type select­ors (e.g. div, p, img) are usu­ally the main cul­prits for style leaks. They have the abil­ity to auto­mat­ic­ally affect a large num­ber of ele­ments as soon as they’re defined.

For example, a style like div { padding: 16px; } is going to change a lot of ele­ments. This power is dan­ger­ous and often leads to mistakes.

A nat­ur­al con­sequence of this wide scope is that there’s a lot of room for acci­dent­ally styl­ing ele­ments you didn’t intend to change. With type select­ors, you’re say­ing​“I want to style all ele­ments which match this gen­er­al pat­tern” instead of​“I want to style this, this and this ele­ment, and noth­ing else”. If you’re not tak­ing steps to delib­er­ately choose the spe­cif­ic ele­ments you want to change, chances are you’re affect­ing more things than you anticipated.

Using ancest­or select­ors (e.g. .header img) helps lim­it the scope of affected ele­ments some what, but it has the dis­ad­vant­age of increas­ing spe­cificity. Also, if the ancest­or is high up in the doc­u­ment tree (e.g. on the body ele­ment), the scope of the type select­or will encom­pass the major­ity of ele­ments on a page anyway.

They increase markup coup­ling #

When we write type select­ors, we are choos­ing a spe­cif­ic type of ele­ment to apply a style to. But what hap­pens to our styles when we decide that our markup isn’t very semant­ic and we change that <span> to a <h3> and that <div> to a <header>? Our type-select­or rules that pre­vi­ously tar­geted those ele­ments no longer will, and our styles will break.

Put anoth­er way, type select­ors increase the coup­ling between your CSS and HTML. They’re very brittle when mak­ing struc­tur­al changes to your markup and require reg­u­lar updates to handle the most basic changes.

Val­id uses of type select­ors #

There are cir­cum­stances where type select­ors are a neces­sary evil. A very com­mon one is when markup is being gen­er­ated dynam­ic­ally, often from a CMS-driv­en rich-text edit­or or mark­down (the les­son con­tent for this guide falls into this cat­egory!). Often the only thing we know about the struc­ture of the markup are the ele­ment types. You may not be able to con­fid­ently use any oth­er type of select­or; IDs and class names often won’t be avail­able because your text edit­or doesn’t sup­port them, or it may be that the authors gen­er­at­ing your con­tent aren’t tech­n­ic­ally-minded, so have no idea what​“IDs” and​“class names” even are.

When we have to use type select­ors… #

… make sure to take steps to lim­it their scope:

See the Pen  Les­son 2: Leak­ing type select­ors by Joe For­shaw (@joeforshaw) on Code­Pen.

Tip 3: Exclus­ively use class select­ors #

Class select­ors can be the anti­dote to a lot of style scop­ing issues, and can also bring a lot of nice bonuses for free.

Why only class select­ors? #

If we always use class select­ors, it greatly reduces the risk of acci­dent­ally styl­ing ele­ments that we didn’t intend to change.

This is because adding a class select­or style is always a two-step process:

  1. Write you class select­or declaration.
  2. Add the class to the ele­ment you want to style.

Both steps are very delib­er­ate actions that allows the author to eas­ily lim­it the scope of the styles to just the ele­ments they give the class to. Con­trast that with type select­or declar­a­tions, which only fol­low step 1. They skip expli­citly select­ing the ele­ments to be styled and instead just say​“style any­thing that look like this”.

In addi­tion, single class select­ors (e.g. .blog-post or .header) have rel­at­ively low spe­cificity so can eas­ily be over­riden elsewhere.

What about ID select­ors? #

IDs select­ors avoid the leaky beha­viour of type select­ors but they’re still best avoided for mul­tiple reasons:

Free doc­u­ment­a­tion #

Get­ting into the habit of adding classes to everything you need to style can ini­tially be chal­len­ging; some ele­ments can rep­res­ent quite abstract things e.g. a div whose sole respons­ib­il­ity is to restrict the max-width of wid­get. But with some prac­tice, it’s a habit that will come.

And with it, comes the nice added bonus of doc­u­ment­a­tion. Every class you add to your HTML is a con­cise descrip­tion of what it is and what con­tent it’s likely to con­tain. It makes your UI com­pon­ents more main­tain­able and con­cep­tu­ally easi­er to understand.

If you had a select­or along the lines of .nav > div > div and the tar­geted ele­ment didn’t have a class, it’s not ini­tially clear what the ele­ment does and what chil­dren it might have. After invest­ig­a­tion, you refact­or the declar­a­tion to tar­get .nav-link-container. Now it’s clear­er from the select­or what it is and what it might con­tain (prob­ably a <a>?).

CSS Mod­ules #

CSS Mod­ules is a pro­ject to com­bat default glob­al scope. It’s core prin­ciple is to make styles defined loc­ally by default; if you wish to glob­ally apply a rule you have to do so delib­er­ately by using a :global pseudo-class.

It is designed to be inter­op­er­able with JavaS­cript. Using JavaScript’s import state­ments, it enables styles to be grabbed from CSS files and applied to spe­cif­ic HTML elements.

import styles from "./style.css";
element.innerHTML = '<div class="' + styles.className + '">';

If you work a lot with React, Vue, Angu­lar or any oth­er JS front-end frame­work, it’s def­in­itely worth look­ing into integ­rat­ing CSS Mod­ules into your pro­jects for an easi­er time man­aging scope.

End of Les­son 2 #

As we’ve seen, glob­al scope is an import­ant found­ing corner­stone of CSS that enables a lot of good for the web. But for front-enders, it can cause undue pain. By being dis­cip­lined in our approach to writ­ing CSS, we can avoid bad pat­terns that come to bite us later on.

Be delib­er­ate in lim­it­ing the scope of your styles. Where pos­sible, prefer class select­ors over wide-reach­ing type selectors.