CSS class? HTML class? What's in a name?
Some people say "CSS class", some say "HTML class". Some say it makes no difference, it's two names for the same thing. But doesn't it really?
The transistor
A long time ago I watched a podcast about AI. I don't recall what the episode is called or what the name of the guest was, so do let me know if you recognize it. For this post, though, it doesn't really matter. I want to recount a story that the guest shared that — for me — serves as a perfect example for the subject of this post.
Suppose we have a transistor. It has two inputs and one output. The inputs and the output can be either 0 or 1. We have the following mapping of inputs to outputs:
- When both inputs are 1, the output is 0
- When both inputs are 0, the output is 0
- When one input is 1 and the other is 0, the output is 1
What operation does this transistor represent? Most people will say "why this is an XOR operation, of course!" But that is if we suppose 1 means true, and 0 means false. What if 1 means false, and 0 means true? In that case, this is an AND operation. In other words, the way we interpret the physical characteristics of the system — the way we label 1 and 0 — changes how we use it, even though nothing physically changed about it.
You may be familiar with the popular joke: "It's not a bug, it's a feature." This joke describes an attempt to avoid addressing an issue by calling it a non-issue. It also shows that how we label things determines the action we are (not) going to take. Language — which is the tool we use to build the mental model of the world — determines our actions.
Indeed, language plays quite an important role in how we approach things. Therefore, calling something a 'CSS class' as opposed to 'HTML class' (or 'class selector') might also play an important role in how we approach styling.
CSS class
The notion of a 'CSS class' is quite old. It was already quite popular when i started learning HTML and CSS (although, luckily I managed to dodge it back then).
The basic premise of the CSS class school is that classes are declared in CSS.
According to the CSS class school, CSS files are like modules in programming
languages. These modules are "imported" into the HTML using the <link> tag,
and the names defined in the module (classes) are applied to the HTML tags using
the class attribute. This is similar to calling a function referred to by the
class name, passing the element as the argument. These 'function calls' can be
composed by applying multiple classes to the same element.
Here's an example of a header using CSS classes:
<header class="flex-col gap-s">
<h2 class="text-xl">Introduction to CSS</h2>
<div class="text-s text-light">Selectors</div>
</header>
HTML class
The 'HTML class' school of thought defines classes as labels for a group of elements that share the same role in the content structure.
According to the HTML class school, classes are declared in the HTML. They are then referenced in the CSS code and associated with one or more declaration blocks. This approach is similar to SQL, where elements are queried using classes (and other selectors), and the resulting, possibly empty, set of elements is styled using the declaration block associated with the query.
Here's an example of a header using HTML classes:
<header>
<h2>Introduction to CSS</h2>
<div class="subtitle">Selectors</div>
</header>
Yes, there are no classes on most elements. We only added a class to the
<div>, because it is ambiguous due to its non-semantic nature. The HTML class
school is not about classes to begin with. It's just saying that classes are
declared in HTML, not that you must use them for everything. Because a class is
seen as just one kind of selector, they are only added where needed to remove
ambiguity.
What's the difference?
Physically there is no difference. Both approaches will work and the browser will not complain. There is no difference in the same sense in which the transistor doesn't really care whether 1 is interpreted as true or false.
However, the way we think about classes makes a difference for how we use them.
Let's suppose we have the following scenario. We have a custom CMS solution that has a WYSIWYG editor. The editor outputs raw HTML which is inserted into the database after sanitization. One of the features of this editor is an article header we used as an example earlier.
One day, the product team decides that the header should use a grass green text with, and the subtitle should be a "smidgen larger".
With CSS classes, this would ideally mean that the <h2> element should gain a
text-green class, and the the <div> should have its text-s class replaced
with text-m class. Since these classes are embedded in the database, we
would need to perform a one-time update on all database rows... every time
we have a change like this.
Suppose we want to avoid doing the data migration for whatever reason. In that case, we would be forced to use the selectors differently. For example we might opt to say something like this to make the subtitle larger:
.text-s {
font-size: 1rem;
}
But now we have a bug. Because the same class is used everywhere, everything sort of became bigger. To address this, we now need to make this selector a bit more specific:
.flex-col.gap-s > .text-s {
font-size: 1rem;
}
Sadly, we still have one more issue. The .text-s class name no longer
accurately describes the font size.
With the HTML classes approach, we don't have the touch the database because the appearance of the header elements are defined entirely in CSS with no modification needed to the HTML. We also don't need to worry about the mismatch between the class name and what they mean, because the class name describes the content, not the appearance.
This example shows why proponents of the HTML class school stick to their approach — separation of concerns: the presentation is entirely constrained to CSS, and anything related to presentation is addressed using CSS alone.
Another difference between the CSS class and HTML class approaches is the way CSS and HTML are edited.
With CSS class approach, we cannot avoid editing HTML at the same time as we edit the CSS. Although the frequency of CSS edits reduces over time as the number of reusable classes increases, we still need to do any new classes in parallel. (This is the problem that Tailwind aims to solve.)
With HTML class approach, HTML is usually not touched once it is completed. Depending on our skill, we may still end up editing HTML to add a few wrappers here and there, or add some classes we forgot to add, but for the most part, wan do this ahead of time once we have enough experience. Once we start styling the elements, we spend nearly 100% of the time editing just the CSS.
Different approaches, different problems
As we've seen in the section solution to the issue described in the previous section, it is quite easy to get into the trap of applying complex selectors just to override something that was badly placed due to the overuse of classes. However, this issue is not specific to CSS class approach.
Unlike CSS class approach, where selectors with high specificity are a result of a forced hand due to aggressive selector reuse, with HTML class school, selectors with high specificity are used deliberately. In theory, this makes the approach more likely to result in specificity issues — and it does happen to novices quite often. With practice, this becomes easier to avoid, though, thanks to the loose relationship between the markup and CSS declarations, which is entirely controlled on the CSS side, and because the choice of selectors is not forced by the markup. In other words, we are not usually forced to use high-specificity selectors if we don't have to.
Let's consider the previous example with artificially inflated specificity:
.text-s {
font-size: 87.5%;
}
.flex-col.gap-s > .text-s {
font-size: 100%;
}
In the HTML class school, we go from:
.subtitle {
font-size: 87.5%;
}
to:
.subtitle {
font-size: 100%;
}
There is no inflation of specificity, there are no overrides. It doesn't even matter what the specificity of the selector is, because all we are doing is changing the value of the property.
This is not to say that the HTML class approach is immune to specificity conflicts. It just means that it's less prone to it, and its practitioners don't encounter such issues to the extent that it would be called 'specificity hell'.
HTML class, on the other hand, has an issue called selector piling. Because of the way code reuse is done with this approach, we may end up having code that looks like this:
.subitle,
.procuts > span:last-child,
.posts h3 > :last-child {
font-size: 100%;
}
In some cases, these can get way out of hand. Sometimes, a bit of scripting
can solve this issue, or something like @extend in SASS, but if we're
hand-coding CSS, it can happen. This is because, unlike in the CSS class
school, selectors don't represent an alias for font-size: 100%. They
represent elements on the page.
In my experience doing both CSS and HTML class approaches, I find that the CSS class approach makes the wrong kind of trade-offs. Although HTML class approach generally requires a little more skill to fully leverage, I find that it makes far less trade-offs and results in a more flexible relationship between CSS and HTML (looser coupling, if you will).
I should also note that most of the modern CSS-related tools such as Styled components, CSS modules and Tailwind, are primarily aimed at addressing the issues with the CSS class approach, even when they claim to solve issues with CSS in general. Some tools such as scoped CSS and BEM solve them partially but introduce new issues unique to their solutions. At this point, I am fully convinced that the only true solution to CSS class issues is to change the point of view completely and start thinking in HTML classes.