CSS is often belittled because it’s naturally global—which programmers wish to avoid—but doing so betrays a lack of understanding of CSS’ foundational premise and greatest strength.
Anybody who has struggled with Cascading Style Sheets (CSS) has seen the memes mocking the experience, like the famous Family Guy GIF or this popular Twitter joke. When all CSS styles are globally scoped, the joke writes itself—try to do one simple thing with CSS and everything goes to hell. How can anybody work like this?!?
As Keith J. Grant puts it: “Just because you expect CSS to be easy, doesn’t mean the language is broken when you find it is not.”
For decades now, global variables have been recognized as difficult to reason about and a source of bugs. So, we avoid them! Today, we take this for granted—often forgetting that it’s not an absolute truth.
In the case of CSS, use of global scope isn’t a bug, it’s a feature! The reason for this is subtle and can present a learning curve to newcomers, but it’s important to understand that it was a deliberate, intentional choice.
A Philosophical Problem, Not A Technical One
Let’s think about what CSS truly does. Its mission is unique. On the web, how do we universally design a document or an app? Not just for different browsers, but devices like mobile, laptops, and even TVs or smartwatches. Not just for screens, but maybe a printed page.
As Miriam Suzanne explains in this enlightening video, we’re designing for an “unknown infinite canvas.” Decades ago, CSS’ creators showed remarkable foresight in making the language platform agnostic and contextual.
CSS isn’t opinionated about where our styles come from. They can happily live together in global scope provided we can get the correct visual result for the context in effect at any given time. This mechanism is called the cascade.
Now Let's Get Technical
All of our CSS rulesets (style declarations grouped under a selector) will co-exist globally. For our parsed HTML, for each element in turn, the browser filters for any styles that might apply, depending on their selectors. Zero, one, or many rulesets will match a given element.
Next, the browser sorts the declarations within these matching rulesets. Any duplicates then present a conflict to be resolved. The cascade is a sorting operation on multiple dimensions that decides which declaration “wins” and gets applied to the element while the others are ignored.
After sorting, the last (or only, if there were no conflicts) instance of each distinct property name is the one whose value gets applied.
The cascade sorts by: (A) CSS property name (group all like-named styles together); then by (B) importance; then by (C) specificity; and finally by (D) source order (literally the position of a declaration in our code). The concepts of importance and specificity deserve a bit more explanation.
Importance is how CSS ranks a declaration based on its origin. Possible ranks, in increasing precedence, are:
- Declarations from the User Agent (UA) stylesheet—a.k.a. browser styles, defining how things should look according to the UI conventions of the operating system or browser (e.g., form fields, buttons).
- Declarations from Author stylesheet(s)—what makes up our imported or embedded custom-written CSS.
- Declarations from the Author annotated with the
!importantkeyword—such declarations are basically exempt from the cascade.
Specificity is a CSS calculation that gives more or less weight to a ruleset’s selector based on the syntax used. For example, a common use of specificity is to assign different classes to an element to extend or vary its styles.
Declarations can progressively increase their specificity by:
- Having a selector that targets at least one element or pseudo-element.
- Having a selector that targets at least one class, pseudo-class, or attribute.
- Having a selector that targets at least one element ID.
- Being an inline style: a declaration defined directly on the element using the
styleattribute. This is the maximum possible specificity for an element.
A Note About Inheritance And Defaults
As a last step, the browser must also define styles for any other property that we didn’t explicitly style ourselves. This is done by inheritance (e.g., fonts, colors, etc. from a parent element) followed by initial values (defaults from the CSS specification and not the same as inUA styles).
The end result of the cascade is that every element is completely styled, whether we wrote a dozen CSS declarations or just one.
A Demo Would Be Nice Right About Now
Let’s explore different scenarios we might encounter in our code to see what the cascade produces. Consider the example of applying color to a paragraph. In each example below, the code becomes more complicated, but if we visualize the cascade as a sorting table of declarations then the “winning” style becomes clear.
Granted, the code here is contrived to produce conflicts. Still, it demonstrates that the outcome of the cascade won’t always follow intuitively from our written code.
As a result, we can easily complicate and over-engineer our CSS. Liberal use of
!important, overly complex selectors, or styles defined out of order and in obscure places would indicate a developer (or team) that doesn’t understand how the cascade works.
A prudent, surgical touch is essential. Our goal is to diligently limit the amount of CSS that might ultimately be ignored by the cascade. The only dead code we introduce should be modest, deliberate, and conditional—styles that hibernate until “activated” by a particular context.
Now, the given context could be anything: a one-off button, the size of the user’s screen, a live interaction from the user, the app’s current state, an OS-level accessibility setting on the user’s device, the user’s chosen device or browser, the time of day, and so on.
When we understand this principle and leverage the cascade, we do less work. We write less code.
Unique Benefits Of Global Scope And The Cascade
Writing less code is admirable enough, but there are other benefits.
First, there are many styles (e.g., typography, color, spacing) that we hope are consistent and predictable. Styling components piecemeal means we’ll inevitably diverge from the original design, bloat our codebase, and create bugs. Design patterns are easily reinforced by proper use of global scope and the cascade, which makes designers and developers happy.
Second, global styles are automatic, making them easy to reuse or extend. Any new feature or page we create can be instantly styled with minimal effort, and then any specific styles can safely expect those global styles to be handled already.
Lastly, CSS obligates us to think holistically. Typical software architecture solves for complexity with encapsulation and modular code. CSS is contextual, so that kind of isolation isn’t always practical—we can’t entirely ignore how anything surrounding our component is styled. This is in fact closer to how designers—and even actual engineers—approach their work.
Why Do We Need To Know This?
Some readers might wonder why any of this matters, given everything that modern CSS offers. We can use convention (BEM), make globals opt-in (CSS Modules), adopt serious tooling (CSS-in-JS), or even heartily embrace global scope (functional CSS). Do we really need to think about the cascade?
Never forget that such tooling may very well be the ultimate leaky abstraction. The user always receives CSS, no matter what fancy tricks we employ to avoid thinking about it ourselves.
So, just as we learn to do algebra by hand before using a calculator in high school, we ought to know how CSS works!
For one thing, every developer at some point will encounter a legacy codebase. Maybe we’re fixing a bug in CSS a stranger wrote years ago, reskinning an app’s design, or migrating to something more modern. This knowledge helps us confidently navigate the existing code and then make it better.
For another, even when our styles are locally scoped, the cascade and issues of scope are still present. Local scope doesn’t magically make our CSS safer. Moreover, we can still employ aspects of the cascade like inheritance or specificity to keep a component’s CSS clean and efficient.
Finally, for web apps, the CSS is our most direct, tangible connection to users—a good user experience will ensure they remember our app fondly. It’s our responsibility to master this technology, accommodate our users’ contexts, and provide the best experience possible.
CSS is far more nuanced than a typical programming language. Most developers aren’t comfortable with nuance and ambiguity, so some degree of apprehension isn’t surprising.
CSS resides somewhere in the space between design and programming. It’s contextual and unassuming, and therefore more philosophically demanding of developers who’d hope to use it wisely. The global nature of CSS isn’t something we can ridicule, grudgingly tolerate, or blunder through.
CSS has its quirks, to be sure, but understanding how the cascade works can benefit both developers and users alike! It’s a sophisticated and proven technology uniquely suited to the challenge of building user interfaces for an unpredictable web.