Lesson 2: Scope
One of the coolest and also most frustrating things about CSS is that any style has the scope to change any aspect of any element on a page (given it has sufficient specificity). We say that they have global scope.
In this lesson we’re going to cover:
- An explanation of how scope ties into CSS’s cascade
- The benefits that scope affords us.
- Some of the drawbacks it brings.
- Tips for avoiding scope catastrophe.
- A brief look at how CSS Modules can help us.
The Cascade #
First, to understand why all CSS rules have global scope, we have to properly understand CSS’s cascade algorithm. It’s an absolutely fundamental part of CSS (the C stands for Cascading!), but what does“cascading” actually mean in this context?
“The Cascade” is how browsers determine which styles to apply to a page. As stylesheets are downloaded, the browser reads each one, top to bottom, and applies visual changes to elements targeted by the individual rules.
Stylesheets are broken down into three main categories:
- User-agent stylesheets — These are your browser’s default styles. The CSS specification isn’t strict on what these should be, so defaults vary quite a lot between different browsers.
- Author stylesheets — The stylesheets produced by the builders of a site. Most of the styles that users on the web see will fall into this category. They’re what make 99% of pages look the way they do.
- User stylesheets — Custom stylesheets that a user can install to tailor the appearance of web pages.
User-agent styles put forth sensible defaults, which are overridden by author stylesheets, which in turn can be overridden by user stylesheets. This is the cascade in a nut shell. Several baseline style sets, which are overridden by increasingly specific styles (as touched on in Lesson 1).
Without global scope, CSS wouldn’t be able to achieve this behaviour. If we were able to exclude certain declarations from being overridden, it would prevent user stylesheets taking precedence over user-agent and author styles.
Advantages of global scope #
User stylesheets provide loads of accessibility benefits. Those of us with visual impairments can inject custom stylesheets to make the web more readable. Be that through larger font sizes, increased colour contrasts or colour-blind-friendly palettes. Pretty cool! Without them and global scope, we’d be excluding millions of people from visually experiencing the web.
In addition to the visually impaired, global scope makes CSS really accessible to those interested in learning web development. You don’t have to learn any complicated API, you just open up your browser developer tools and start tinkering. You can change anything! Such power! Such results! This instant, low-barrier feedback loop of CSS can really catapult those curious enough into web development.
Disadvantages of global scope #
For reasonably-sized sites that require a team of engineers to maintain, global scope is more often than not a recipe for disaster. Almost all front-end engineers at one point in their careers will add/update/remove a style, only to find it accidently leaks into some other section of another page.
Example #
Take the above example for instance. We’ve taken reasonable steps to scope our styles to just elements that have a .blog-heading
class. But that doesn’t stop another engineer bulldozing their styles into our page. The high-specificity, wide-scoped rule has overidden the intended look of our headings. 😑
Modularity #
Global scope makes any concept of modularity in CSS very difficult. Every declaration has the potential to affect any element it likes, so it’s impossible to completely protect your UI from modification, be it deliberate or accidental. Tomorrow another engineer could stick in a few !important
styles and scrap your nicely-encapsulated, reusable widget.
If you told any back-end engineer that they had to use a programming language that gave all variables global scope, made every object’s internal state visible, allowed any other engineer to override their code, they’d probably have a few choice words for you. But that’s the difficult reality of CSS development, everything’s up for grabs.
Tips for managing scope #
So we’ve talked a bit about why global scope is important for a lot of web users and also how it’s a pain for engineering teams. But we need to find a nice middle-ground that helps both parties.
On the users’ side, we have browsers and the CSS specification that ensure user stylesheets will always have the scope to override existing styles. On the engineering side, we have us, the style-smiths. It’s up to us to figure out strategies for managing global scope in a maintainable way.
As rules can touch anything they like, the best we can hope for is to avoid writing styles that accidentally leak into other parts of our UI. This takes diligence and team discipline; if just one engineer isn’t pulling in the right direction it’ll result in a mess.
Whatever the solution, to manage scope effectively, we need to adhere to the following general principle:
Keep scope limited and deliberate
We have to limit the scope of our changes to just the elements we deliberately want to change. New styles should only affect the things we want and nothing more. No unintended side-effects, no headaches. So how do we do this?
Tip 1: Separate stylesheets for each page? 🤔 #
One possible solution that feels right initially is to have separate spreadsheets for each page and only include the styles we need and nothing more. That way there’s less chance of styles written for a specific page leaking outside to other pages.
However in reality, understanding which styles a page requires, and which styles it doesn’t, is a very hard task, especially if you’re a big team. It requires everyone to hold in their head every possible UI element that could possibly appear on a page so that they can load in the right styles. That’s a really unscalable situation, so I wouldn’t recommend it for managing scope.
Often when stylesheets grow so large that they start to have noticeable browser performance issues, pruning out unused styles is a sensible decision. But this is rarely done at a page-by-page level, more often it’s per section, due to the challenges described above. Therefore, using it as a method for limiting scope won’t be effective as leaks can still run rampant within those sections.
Tip 2: Avoid type selectors #
Rulesets that use type selectors (e.g. div
, p
, img
) are usually the main culprits for style leaks. They have the ability to automatically affect a large number of elements as soon as they’re defined.
For example, a style like div { padding: 16px; }
is going to change a lot of elements. This power is dangerous and often leads to mistakes.
A natural consequence of this wide scope is that there’s a lot of room for accidentally styling elements you didn’t intend to change. With type selectors, you’re saying“I want to style all elements which match this general pattern” instead of“I want to style this, this and this element, and nothing else”. If you’re not taking steps to deliberately choose the specific elements you want to change, chances are you’re affecting more things than you anticipated.
Using ancestor selectors (e.g. .header img
) helps limit the scope of affected elements some what, but it has the disadvantage of increasing specificity. Also, if the ancestor is high up in the document tree (e.g. on the body
element), the scope of the type selector will encompass the majority of elements on a page anyway.
They increase markup coupling #
When we write type selectors, we are choosing a specific type of element to apply a style to. But what happens to our styles when we decide that our markup isn’t very semantic and we change that <span>
to a <h3>
and that <div>
to a <header>
? Our type-selector rules that previously targeted those elements no longer will, and our styles will break.
Put another way, type selectors increase the coupling between your CSS and HTML. They’re very brittle when making structural changes to your markup and require regular updates to handle the most basic changes.
Valid uses of type selectors #
There are circumstances where type selectors are a necessary evil. A very common one is when markup is being generated dynamically, often from a CMS-driven rich-text editor or markdown (the lesson content for this guide falls into this category!). Often the only thing we know about the structure of the markup are the element types. You may not be able to confidently use any other type of selector; IDs and class names often won’t be available because your text editor doesn’t support them, or it may be that the authors generating your content aren’t technically-minded, so have no idea what“IDs” and“class names” even are.
When we have to use type selectors… #
… make sure to take steps to limit their scope:
- A good rule of thumb is to use the direct parent of the targeted element in the type selector, e.g.
.blog-post p
. This will bump up the specificity of the rule, but it’s a compromise that needs to be made to limit the scope. - If your dynamic elements don’t have a deep nested structure and are immediate children to an element with a class name, definitely use the
>
child combinator. This will further limit their scope, with no increase to specificity. When you mix-and-match between dynamic content and static content (like these lessons do!) this prevents styles leaking into where they shouldn’t. If you don’t, something like this might happen:
Tip 3: Exclusively use class selectors #
Class selectors can be the antidote to a lot of style scoping issues, and can also bring a lot of nice bonuses for free.
Why only class selectors? #
If we always use class selectors, it greatly reduces the risk of accidentally styling elements that we didn’t intend to change.
This is because adding a class selector style is always a two-step process:
- Write you class selector declaration.
- Add the class to the element you want to style.
Both steps are very deliberate actions that allows the author to easily limit the scope of the styles to just the elements they give the class to. Contrast that with type selector declarations, which only follow step 1. They skip explicitly selecting the elements to be styled and instead just say“style anything that look like this”.
In addition, single class selectors (e.g. .blog-post
or .header
) have relatively low specificity so can easily be overriden elsewhere.
What about ID selectors? #
IDs selectors avoid the leaky behaviour of type selectors but they’re still best avoided for multiple reasons:
- IDs are designed to be unique to each page, so they limit a style’s reusability should you want to style multiple occurrences of an element on a page.
- An element should only have one ID, so you can’t assign multiple IDs to an element like you can with classes e.g.
<div id="foo bar">
nope 👎,<div class="foo bar">
yep 👍. - Their specificity is prohibitively high, so they make future changes to an element harder to override.
Free documentation #
Getting into the habit of adding classes to everything you need to style can initially be challenging; some elements can represent quite abstract things e.g. a div
whose sole responsibility is to restrict the max-width
of widget. But with some practice, it’s a habit that will come.
And with it, comes the nice added bonus of documentation. Every class you add to your HTML is a concise description of what it is and what content it’s likely to contain. It makes your UI components more maintainable and conceptually easier to understand.
If you had a selector along the lines of .nav > div > div
and the targeted element didn’t have a class, it’s not initially clear what the element does and what children it might have. After investigation, you refactor the declaration to target .nav-link-container
. Now it’s clearer from the selector what it is and what it might contain (probably a <a>
?).
CSS Modules #
CSS Modules is a project to combat default global scope. It’s core principle is to make styles defined locally by default; if you wish to globally apply a rule you have to do so deliberately by using a :global
pseudo-class.
It is designed to be interoperable with JavaScript. Using JavaScript’s import
statements, it enables styles to be grabbed from CSS files and applied to specific HTML elements.
import styles from "./style.css";
element.innerHTML = '<div class="' + styles.className + '">';
If you work a lot with React, Vue, Angular or any other JS front-end framework, it’s definitely worth looking into integrating CSS Modules into your projects for an easier time managing scope.
End of Lesson 2 #
As we’ve seen, global scope is an important founding cornerstone of CSS that enables a lot of good for the web. But for front-enders, it can cause undue pain. By being disciplined in our approach to writing CSS, we can avoid bad patterns that come to bite us later on.
Be deliberate in limiting the scope of your styles. Where possible, prefer class selectors over wide-reaching type selectors.
Lessons
- Lesson 1: Specificity
- Lesson 2: Scope ← You're here
- Lesson 3: Naming
- Lesson 4: Variations
- Lesson: BEM Coming Soon
- Lesson: SCSS Coming Soon
-
Lesson:
position
Deep Dive Coming Soon - Lesson: Reusability Coming Soon
- Lesson: Units Coming Soon
- Lesson: Media Queries Coming Soon
-
Lesson:
display
Deep Dive Coming Soon - Lesson: Flexbox Coming Soon
- Lesson: Grid Coming Soon
- Lesson: Inheritance Coming Soon
- Lesson: Layout vs Aesthetics Coming Soon
- Lesson: Fixing Bad CSS Coming Soon
- Lesson: Composability Coming Soon
- Lesson: File Organisation Coming Soon
- Lesson: Accessibility Coming Soon