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.
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
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
.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>
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>
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
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
.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>
Most CSS declarations have a parent/child relationship, it either:
color
, line-height
, etc.display
, position:relative
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>
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.
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>
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;
}
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
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.
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.