Scope in CSS

I am likely going to write a “CSS for JavaScripters” book, and therefore I need to figure out how to explain CSS to JavaScripters. This series of article snippets are a sort of try-out — pre-drafts I’d like to get feedback on in order to figure out if I’m on the right track.

Today we treat the knotty problem of CSS selector scope. JavaScripters rightly feel that the fact that all CSS selectors are in the global scope complicates their applications. What can we do about it?

I wrote the explainer below. As usual, I’d love to hear if this explainer works for you or confuses you.

Global scope

Any CSS selector is valid throughout the document. If you use p span it selects any span in a p in the entire document. Here, every span in a p in the document has a red background.

p span {
	background-color: red;
}

Sometimes you don’t want that. Sometimes you want to select only spans in ps in your specific module. This is the core of the scope problem in CSS: how to restrict your style definitions to only the module you’re working on.

The fundamental solution is adding a class name or ID before the actual selector; something like #myModule p span. Now the declaration is scoped to the myModule element.

In this code example, spans in ps are red, except when they’re in the myModule element; then they’re blue.

#myModule {
	border: 1px solid black;
	padding: 1em;
}

p span {
	background-color: red;
}

#myModule p span {
	background-color: blue;
}

Yes, scoping sort-of works, but your CSS file becomes a jumbled mess of selectors, overrules, and other stuff. For intance, where exactly do you put the #myModule p span rule? I always place it after the global selector it overrules, as in the example above, but I can see why people would set it straight after #myModule. Fun times ahead when you’re taking over someone else’s CSS.

Scoping CSS selectors with sane, readable syntax is one of the purposes of the many CSS-in-JS solutions that are now vying for developer attention. (Another purpose is making sure that scoping IDs aren’t repeated — if your HTML contains two elements with id="myModule" you have the same scoping problem all over again.)

However, it appears that the CSS scoping problem will be solved in the not-too-distant future. A limited local scope is already present, and a full solution is in the making.

CSS custom properties

CSS custom properties (also called CSS variables) already have scope. It is possible to redefine the value of a custom property for a specific element and its descendants.

Take this example:

body {
	--color: red;
}

#myModule {
	border: 1px solid black;
	padding: 1em;
	--color: blue
}

p span {
	background-color: var(--color);
}

Now a span in a p has a red background, except in myModule, where it has a blue background. In practice, the html and body elements count as the global scope; most other elements as a local scope.

Custom properties can hold any valid CSS value, and they work for any declaration. So we could extend the example like this:

body {
	--color: red;
	--spandisplay: inline;
}

#myModule {
	border: 1px solid black;
	padding: 1em;
	--color: blue
	--spandisplay: block;
}

p span {
	background-color: var(--color);
	display: var(--spandisplay);
}

Now spans in ps in myModule have display: block, while spans in ps elsewhere have display: inline.

There is no doubt that true local scope has landed in CSS with the addition of custom properties. There is also no doubt that the system can be clunky when you want to use local scoping on a massive scale.

In theory it’s possible to define a gazillion custom properties in myModule for internal use, and define the same properties with different values in otherModule. However, this assumes that, internally, the modules will have the same structure. For instance, the simple example above assumes that every module in the entire page uses p span in a similar way. If otherModule would use p strong instead, you’d have to add new declarations. All this is not impossible, but it’s questionable whether it solves CSS’s scoping problem in an easy-to-use way.

So all in all the verdict is mixed. Yes, custom properties bring local scope to CSS, but it’s best used in small, subtle ways instead of the wholesale, system-wide way JavaScripters would like to see. [Unless I’m missing something here. If I do, please tell me. I need to know.]

CSS nesting

The CSS Nesting specification offers a new way of sort-of creating sort-of local CSS selectors. Note that this spec is relatively new, and it will take a little while before browsers support it.

CSS preprocessors have done nesting for ages now, and the proposed syntax is quite close to SASS:

p span {
	background-color: red;
	/* display: inline is the default anyway; no need to define it */
}

#myModule {
	border: 1px solid black;
	padding: 1em;
	& p span {
		background-color: blue;
		display: block;
	}
}

The & p span evaluates to #myModule p span; in other words, the & adds the parent selector. Note that the & is required: CSS needs to know where to insert the parent selector.

On the one hand, CSS Nesting offers syntactic sugar, but nothing fundamentally new. We already saw how to write the example above in today’s CSS:

On the other hand, the nested variant is much more readable, and offers you a simple way of stating whether a certain selector is local or global. It is totally clear that & p span is a local selector, and that its styles will not apply outside myModule. The p span outside the block, on the other hand, is clearly global.

This makes it a lot easier to see the scope of your selectors at a glance, and to subdivide your CSS files. If you’re working on myModule, you add styles to the myModule block. If you define styles outside that block, it’s clear that they are global.

Would CSS nesting help?

Today’s question to JavaScripters is: do you think that CSS Nesting will make your work significantly easier? It seems so to me, but I am an old-guard front-end developer with little knowledge of the modern JavaScript ecosystems, so I might miss something.

This is the blog of Peter-Paul Koch, web developer, consultant, and trainer. You can also follow him on Twitter or Mastodon.
Atom RSS

Categories:

Monthlies: