Semantic CSS Selectors

CSS and HTML are co-dependent documents, and writing two very different documents simultaneously is hard. Can they be written semantically? 6 December 2019

# Background

Writing manageable CSS and HTML is particularly difficult. Not only are there many ways to syntactically produce the same result, there are many more ways to semantically convey that intent too.

In this article, I'll style a very basic modal with various CSS approaches, shifting the complexity between HTML and CSS, searching for the most semantic way of representing both.

# Example 1: CSS Depends on HTML

Let's start with one extreme. In this version, the HTML is clean with only a single CSS class. The CSS then becomes very heavily dependent on the HTML and is not very portable. It is burdened with unpredictable specificity, making it very difficult to reason with.

This strategy may be useful or even needed when we need to have very little complexity in the HTML. For example in many CMSes, the generation of the HTML is indirect, perhaps through a non-HTML representation like Markdown. So, don't shoot it down just yet.

.dialog {
align-items: center;
background-color: rgba(0, 0, 0, 0.75);
bottom: 0;
display: flex;
justify-content: center;
left: 0;
position: fixed;
right: 0;
top: 0;
}

.dialog > article {
background-color: #fff;
max-width: 40rem;
width: 90%;
}

.dialog > article > header {
background-color: #21366C;
color: #fff;
cursor: pointer;
padding: 1rem;
}

.dialog > article > header::after {
content: "×";
float: right;
}

.dialog > article > section {
padding-left: 1rem;
padding-right: 1rem;
}

.dialog > article > footer {
padding-left: 1rem;
padding-right: 1rem;
}

.dialog > article > footer > ul {
background-color: #000;
padding: 0.5rem;
}

.dialog > article > footer > ul > li {
color: #fff;
display: inline-block;
list-style-type: none;
}

.dialog > article > footer > ul > li > a {
color: inherit;
}
<div class="dialog">
<article>
<header>
<h1>header</h1>
</header>
<section>
<p>body</p>
</section>
<footer>
<ul>
<li>
<a href="#">link</a>
</li>
<li>
<a href="#">link</a>
</li>
</ul>
</footer>
</article>
</div>

# Example 2: HTML Depends on CSS

Going the other extreme, we clean up the CSS with uniform specificity of a single class (not counting pseudo-classes and attributes). The complexity shifts to the HTML; it becomes heavily dependent on the CSS and is not very portable.

.dialog {
align-items: center;
background-color: rgba(0, 0, 0, 0.75);
bottom: 0;
display: flex;
justify-content: center;
left: 0;
position: fixed;
right: 0;
top: 0;
}

.modal {
background-color: #fff;
max-width: 40rem;
width: 90%;
}

.header {
background-color: #21366C;
color: #fff;
cursor: pointer;
padding: 1rem;
}

.header::after {
content: "×";
float: right;
}

.body {
padding-left: 1rem;
padding-right: 1rem;
}

.footer {
padding-left: 1rem;
padding-right: 1rem;
}

.navigation {
background-color: #000;
padding: 0.5rem;
}

.navigation-item {
color: #fff;
display: inline-block;
list-style-type: none;
}

.link {
color: inherit;
}
<div class="dialog">
<article class="modal">
<header class="header">
<h1>header</h1>
</header>
<section class="body">
<p>body</p>
</section>
<footer class="footer">
<ul class="navigation">
<li class="navigation-item">
<a class="link" href="#">link</a>
</li>
<li class="navigation-item">
<a class="link" href="#">link</a>
</li>
</ul>
</footer>
</article>
</div>

# Example 3: Single Responsibility Principle

The practise of using one class per element is attractive, as per Example 2 above, but it also encourages CSS selectors that violate the Single Responsibility Principle. That is, selectors that contains unrelated declarations. Duplicate declarations in JS are often summarily factored out, but somehow often missed in CSS.

For example, header from Example 2 above can be split into separate responsibilites. Firstly, to add horizontal padding, and secondly to colour and add vertical padding. This separation allows the responsibility for horizontal padding to be shared amongst other similar part elements in Example 3 below.

The composes keyword in CSS Modules enables writing a single class in the HTML, and processes the CSS by copying the common declarations programmatically. But this abstraction makes it harder to reason about the semantic representation of the HTML without also referring to the CSS. Ideally, we should be able to mentally visualise what the end result might look like just by looking at the HTML alone. (Not bashing it, CSS Modules have other capabilites that I think are useful though.)

.dialog {
align-items: center;
background-color: rgba(0, 0, 0, 0.75);
bottom: 0;
display: flex;
justify-content: center;
left: 0;
position: fixed;
right: 0;
top: 0;
}

.modal {
background-color: #fff;
max-width: 40rem;
width: 90%;
}

.part {
padding-left: 1rem;
padding-right: 1rem;
}

.header {
background-color: #21366C;
color: #fff;
cursor: pointer;
padding-bottom: 1rem;
padding-top: 1rem;
}

.header::after {
content: "×";
float: right;
}

.navigation {
background-color: #000;
padding: 0.5rem;
}

.navigation-item {
color: #fff;
display: inline-block;
list-style-type: none;
}

.link {
color: inherit;
}
<div class="dialog">
<article class="modal">
<header class="part header">
<h1>header</h1>
</header>
<section class="part">
<p>body</p>
</section>
<footer class="part">
<ul class="navigation">
<li class="navigation-item">
<a class="link" href="#">link</a>
</li>
<li class="navigation-item">
<a class="link" href="#">link</a>
</li>
</ul>
</footer>
</article>
</div>

# Example 4: Hierarchical relationships

Most CSS declarations have a parent/child relationship, it either:

  • affects the selected element: eg. color, line-height, etc.
  • affects the child element: eg. display, position:relative
  • needed in conjunction with a parent: eg: position:absolute

Recognising this (or other sibling relationships), we can reduce the classes verbiage with the universal child selector (> *). The HTML in Example 4 below is a lot leaner than Example 3 above. With fewer class attributes in the HTML, they become more significant, and we gain more semantic value in them.

.dialog {
align-items: center;
background-color: rgba(0, 0, 0, 0.75);
bottom: 0;
display: flex;
justify-content: center;
left: 0;
position: fixed;
right: 0;
top: 0;
}

.dialog > * {
background-color: #fff;
max-width: 40rem;
width: 90%;
}

.sections > * {
padding-left: 1rem;
padding-right: 1rem;
}

.title {
background-color: #21366C;
color: #fff;
cursor: pointer;
padding-bottom: 1rem;
padding-top: 1rem;
}

.title::after {
content: "×";
float: right;
}

.navigation {
background-color: #000;
padding: 0.5rem;
}

.navigation > * {
color: #fff;
display: inline-block;
list-style-type: none;
}

.link {
color: inherit;
}
<div class="dialog">
<article class="sections">
<header class="title">
<h1>header</h1>
</header>
<section>
<p>body</p>
</section>
<footer>
<ul class="navigation">
<li>
<a class="link" href="#">link</a>
</li>
<li>
<a class="link" href="#">link</a>
</li>
</ul>
</footer>
</article>
</div>

# Example 5: CSS Cascade

Many developers eschew the CSS cascade entirely because it can quickly become unmanageable when there are too many of them. But if only a modest number of specificity levels are used, we stand to gain DRY and semantic code.

I'd suggest using only the following levels, corresponding to large step increases in specificity.

  • specificity 0000: universal
  • specificity 0001: elements, and pseudo-elements
  • specificity 001x: classes, pseudo-classes, and attributes
  • specificity 01xx: IDs
  • specificity 1xxx: inline styles

By using lower specificity, we can convey greater meaning, like "all colour is inherited from the parent", or "all h1 have this font and colour". And we can do this without having to repeat the same declaration in multiple higher specificity selectors.

* {
color: inherit;
}

.dialog {
align-items: center;
background-color: rgba(0, 0, 0, 0.75);
bottom: 0;
display: flex;
justify-content: center;
left: 0;
position: fixed;
right: 0;
top: 0;
}

.dialog > * {
background-color: #fff;
max-width: 40rem;
width: 90%;
}

.sections > * {
padding-left: 1rem;
padding-right: 1rem;
}

.title {
background-color: #21366C;
color: #fff;
cursor: pointer;
padding-bottom: 1rem;
padding-top: 1rem;
}

.title::after {
content: "×";
float: right;
}

.navigation {
background-color: #000;
padding: 0.5rem;
}

.navigation > * {
color: #fff;
display: inline-block;
list-style-type: none;
}
<div class="dialog">
<article class="sections">
<header class="title">
<h1>header</h1>
</header>
<section>
<p>body</p>
</section>
<footer>
<ul class="navigation">
<li>
<a href="#">link</a>
</li>
<li>
<a href="#">link</a>
</li>
</ul>
</footer>
</article>
</div>

# CSS Custom Properties

When many properties share the same value, we can use CSS custom properties to provide another level of indirection. In the example below, --spinner-thickness and --spinner-size is common to several properties, and thus factored out. When the class spinner is used alone, we get a small spinner, but the classes spinner spinner--large are used, we get a larger one. (In this case, the larger size has precedence because it has a later source order.)

Using this method won't reduce the number of classes needed in the HTML, but it can help improve the semantic value of the CSS as it allows us to have named values. When these values need to change, either through media queries, modifier classes, or even programmatically in Javascript, we only need to change the custom property value.

:root {
--brand-primary: #21366C;
--brand-primary-grey: #888888
}

.spinner {
--spinner-thickness: 0.2rem;
--spinner-size: 2rem;

align-items: center;
display: flex;
height: 0;
justify-content: center;
}

.spinner::after {
animation: delayed 1s, spin 2s linear infinite;
border: var(--spinner-thickness) solid var(--brand-primary-grey);
border-radius: 50%;
border-top: var(--spinner-thickness) solid var(--brand-primary);
content: "";
height: var(--spinner-size);
width: var(--spinner-size);
}

.spinner--large {
--spinner-thickness: 0.5rem;
--spinner-size: 5rem;
}

# What is a component?

There is a growing number of libraries, frameworks, and developers who prefer and recommend a component based grouping of files and content. In this context, HTML and CSS are colocated, either in a common per-component folder, or even within a Single File Component.

By embracing the Single Responsibility Principle above for CSS, you'll find that the vast majority of CSS selectors become utility based – to set a particular colour, spacing, alignment, etc. and UI components use multiple CSS selectors to achieve its desired look and feel. The component based structure breaks down under these circumstances.

# Summary & Conclusion

I've seen many projects use one class per element, and thus result in selectors with too much responsibility. These same projects also often have so many classes, that developers start ignoring them when reading the HTML. Before long, these projects quickly grow large, and it becomes harder and harder to refactor the CSS (and HTML).

It's a shame that the art of writing semantic CSS (and HTML) has very little focus on the internet. The rising popularity of various CSS-in-JS techniques exacerbates the situation, as the increasing abstraction makes it even harder to reason about the resulting CSS and HTML. I hope this article shows that you writing semantic CSS is indeed possible.