Halvor William Sanden
Colour modes

Making components with CSS nesting

CSS nesting can help us write better frontends by moving our approach towards thinking more about the bigger picture. It also makes working with fewer class names easier, but does it make BEM and other naming conventions obsolete?

A list component #

One of the areas I have overused classes in the past is lists. Whether it’s been plain types or richer navigation, I have made classes for every element and state. I could have chained selectors, but if the class names are long or the BEM conventions are strong, it often ends up with .listname__element, which makes the HTML severely verbose.

Lists have such a strict structure that we can lean on an ancestor class and then element selectors the rest of the way. Nesting also works nicely for making variants.

Let’s look at a few different ways to make a keyword list with two different item types: tags that are links and badges that are just text.

HTML and CSS #

The base HTML will be the same with a span for the badge and an anchor element for the tag. We will also have classes, but their names will be a bit different from one approach to the next.

<ul>
	<li>
		<span-or-a>Keyword</span-or-a>
	</li>
</ul>

The declaration for each element could be:

ul {
	list-style: none;
	margin: 0;
	padding: 0;
	display: flex;
	flex-flow: row wrap;
	gap: 0.5em;
}

li {
	flex: 0 1 auto;
}

span,
a {
	background: lightgray;
	border-radius: 3px;
	padding: 0.25em;
	display: block;
}

The span-business can be a bit controversial, setting it to block means we could have just used a div instead, but we would still set that to block because it shares declaration with the a. The point is to fill the list item completely. This makes the CSS more reusable for now; we avoid conflict or complexity of styling the li and then the a. We will get back to how we can do both in one.

We’re assuming the anchor element inherits global styling for color and text-decoration. States are excluded here for simplicity.

JS component #

With a JS-influenced component, we could end up with selectors like:

.keywords {}
li {}
span, a {}

Not too bad, and it gives us tidy files, except there tend to be lots of them, and the naming convention of the bundled piece becomes an eyesore turning into something like KeywordsComponent__keywords. If a hash is added to ensure it’s unique, we’re also looking at something longer and slightly more difficult to debug.

To avoid using a span, we could add a prop that told the component if it should render tags or badges and add logic based on that. But would it be worth increasing the complexity to have JS do a CSS job? Nay.

BEM #

With BEM, selectors become readable but long and chained.

.keywords {}
.keywords__item {}
.keywords__item--badge span {}
.keywords__item--tag a {}

In the HTML, each list item would have the combined classes of keywords__item keywords__item--badge, accumulating to a lot of code.

If we had an element outside the list, we would see one of the method’s weaknesses, we are limited to two levels, block and element, they don’t have to be parent and child, but we spend some time figuring out how to spread the classes out or wondering if we should start a new BEM instance. If there was another level, I would have put keywords on that and named the list keywords__list.

Nothing stops us from chaining selectors to avoid the E and M, but it’s a solution that might come more naturally from the following example.

CSS nesting #

With nesting, we take some pieces from both BEM and JS naming. First, a plain one:

.keywords {
	& li {}
	& span,
	& a {}
}

We could also eliminate the span and add a class to the list item when there is no link.

.keywords {
	& li {}
	.badge,
	& a {}
}

Notice that we have the badge class with the a selector, but we can also nest them inside the li. The following nesting enables us to set a parent variant and child in the same declaration.

.keywords {
	& li {
		&.badge,
		& a {}
	}
}

Nesting, and CSS in general, isn’t about mirroring HTML; we want a balance of scope and reusability. CSS is a way to select elements in various ways; it’s not a markup layer. While the last example might be unnecessary, it’s relevant as a path to the following method.

CSS nesting with has() and not() #

We can skip the extra class by using the functional has() and not() pseudo classes to see what the list item doesn’t contain. CSS is especially fun to write when the ancestor and descendant can have the same styling based on whether or not the descendant exists. In this case, the li gets the same styling as the a when a doesn’t exist.

.keywords {
	& li {
		&:not(:has(a)),
		& a {}
	}
}

Solve the problem #

There are even more options – it’s CSS, after all. But that doesn’t mean it’s a complete mess of functionality, many features can solve the same thing, but each feature has a specific purpose, things only it can solve.

What we choose depends on what we are trying to build, not only on the individual component level; we must take the extent of the piece’s usage into account, maybe even the entire project – where this piece is going to be used or if there will be variants we need to tell it to render explicitly.

Selective BEM #

BEM is still a useful way to name classes that are related; systemised naming conventions will not become obsolete and many of them embrace nesting. The examples in this article are simple in that they have a strict structure with different elements. In a setting with a looser structure, we will have to use classes that are sufficiently unique.

If we look at a different example where we have used only classic BEM classes as selectors, it could look like the following:

.news {}
.news__entry {}
.entry__title {}
.entry__back {}

Then, we see that entry is an article element and title is h2; we refactor it and use nested elements. It might become something like:

.news {
	& article {}
	& h2 {}
	.back {}
}

But the back class name is relatively generic and could easily cause conflict. A more secure option would be to reintroduce a block name from BEM:

.news {
	& article {}
	& h2 {}
	.news__back {}
}

The same point cannot be made for article and h2 because those are less general. We are guaranteed to have an h2 style already, and we want to inherit something from that. article is an element used and styled specifically; creating a global style for it is unusual.

Re-useful for us #

There’s nothing new happening here for the users. We could have written this ten years ago using chained selectors – except for the has() and not() part – but CSS nesting helps us move towards better code reuse practices. It simplifies pseudo-based selectors and clarifies ancestor-descendant relations. I explore this further in CSS context variants where the list example component is extended to work as a sub-navigation.

As with other programming languages, new CSS features, like nesting, add to the practices that have come before more than they replace them. Increased possibilities help solve different cases better instead of solving everything in one way.


You can fully write CSS without nesting, it’s fairly new and not considered necessary knowledge to produce good interfaces. At the time of writing, I don’t recommend shipping nesting code. While it’s getting closer to complete browser support, it requires transpiling for a while still. I use PostCSS with the postcss-nesting plugin.

The has() functional pseudo selector is also not supported by all yet, there is no way around that, so evaluate if it’s needed or if your users can live with it not working. not() has been supported since early 2021 and is considered safe to use. Both are considered fairly advanced and should be used in specific cases. Always consult your site’s browser stats.

Resources #