Skip to Content

In defence of "semantic css" and the separation of concerns.

We are currently in the process of redesigning our website at work. One of the many conversations that came up was what frontend framework to use. Tailwind, inevitably, was one of those suggested and I had to marshal my thoughts on why I thought it was a very poor choice while still remaining civil. Others, such as Jared White and Aleksandr Hovhannisyan have written eloquently on many of the shortcomings of utility class CSS frameworks, but I would like to focus on a single aspect of the argument in favour of (and against) them: the separation of concerns between HTML and CSS.

This, then, is partially an article about why I think utility class CSS frameworks are a poor choice, but it's primarily an article about how I feel we may have misrepresented the separation of concerns. Any opinions, of course, are my own.

What is a utility class framework?

First of all, what is a utility class framework? In his blog post introducing tailwind css Adam Wathan, Tailwind's creator, gives a good overview of the differences between "semantic" CSS and utility class frameworks. I recommend you read this article if you have not already done so, as it will give you a good idea for the arguments in favour of utility class frameworks. I will only briefly touch on a few of the points Adam makes here.

In a nutshell, semantic CSS is how you were taught to use CSS. You have styles for generic elements, that are then overridden for particular components. Most importantly, you should try, wherever possible, to avoid contaminating your HTML with any design information; all class names should describe what an element is, not what it looks like.

An example of a button element might be:

<style>
    .button {
        display: inline-block;
        padding: 0.5rem 1.5rem;
        border-radius: 0.25rem;

        background-color: rgb(37 99 235);
        color: rgb(255 255 255);

        font-weight: 500;
        font-size: 0.75rem;
        line-height: 1.25;
        text-transform: uppercase;
    }

    .button:hover,
    .button:focus {
        background-color: rgb(29 78 216);
    }

    .button:active {
        background-color: rgb(30 64 175)
    }
</style>

<button class="button">Button</button>

A utility class framework, in contrast, operates on the principle of having classes perform a single function, such as setting a certain amount of padding, or converting an element into a flex-container. In order to build up the styling for an element, you need to combine multiple classes together. In this methodology, of course, HTML and design are very closely coupled; most classes in a utility class framework are named more or less after what they do. An example of the same button created with Tailwind is:

<style>
    .inline-block {
        display: inline-block;
    }

    .px-6 {
        padding-left: 1.5rem;
        padding-right: 1.5rem;
    }

    .py-2 {
        padding-top: .5rem;
        padding-bottom: .5rem;
    }

    .bg-blue-600 {
        background-color: rgb(37 99 235);
    }

    .text-white {
        color: rgb(255 255 255);
    }

    .font-medium {
        font-weight: 500;
    }

    .text-xs {
        font-size: .75rem;
        line-height: 1rem;
    }

    .leading-tight {
        line-height: 1.25;
    }

    .uppercase {
        text-transform: uppercase;
    }

    .rounded {
        border-radius: 0.25rem;
    }

    .hover\:bg-blue-700:hover {
        background-color: rgb(29 78 216);
    }

    .focus\:bg-blue-700:focus {
        background-color: rgb(29 78 216);
    }

    .active\:bg-blue-800:active {
        background-color: rgb(30 64 175);
    }
</style>

<button
    class="inline-block px-6 py-2 bg-blue-600 text-white font-medium text-xs leading-tight uppercase rounded hover:bg-blue-700 focus:bg-blue-700 active:bg-blue-800">
    Button
</button>

For a single component, like this, utility class frameworks obviously produce a lot more code. The idea is that, by splitting classes up to only affect a single property, CSS code duplication is massively reduced on large websites with lots of components sharing common declarations.

Adam makes many more arguments in favour of Tailwind in his blog post that are out of the scope of this piece to go into, but the one that most interested me was his idea that.

I had "separated my concerns", but there was still a very obvious coupling between my CSS and my HTML. Most of the time my CSS was like a mirror for my markup; perfectly reflecting my HTML structure with nested CSS selectors.

My markup wasn't concerned with styling decisions, but my CSS was very concerned with my markup structure.

Maybe my concerns weren't so separated after all.

The argument that Adam is making here is that, especially on large and complex sites, purely semantic CSS is bound to start to reflect the underlying HTML structure. The example he gives is this one for an author bio card component.

<style>
    .author-bio {
        background-color: white;
        border: 1px solid hsl(0, 0%, 85%);
        border-radius: 4px;
        box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
        overflow: hidden;
    }

    .author-bio>img {
        display: block;
        width: 100%;
        height: auto;
    }

    .author-bio>div {
        padding: 1rem;
    }

    .author-bio>h2 {
        font-size: 1.25rem;
        color: rgba(0, 0, 0, 0.8);
    }

    .author-bio>p {
        font-size: 1rem;
        color: rgba(0, 0, 0, 0.75);
        line-height: 1.5;
    }
</style>

<div class="author-bio">
    <img src="https://cdn-images-1.medium.com/max/1600/0*o3c1g40EXj65Fq9k." alt="">
    <div>
        <h2>Adam Wathan</h2>
        <p>
            Adam is a rad dude who likes TDD, Active Record, and garlic bread with cheese. He also hosts a decent
            podcast and has never had a really great haircut.
        </p>
    </div>
</div>

If our CSS inevitably comes to reflect the HTML, why not write it in a way which minimises duplication and only allows developers to pick from a pre-set selection of values when writing their styles? That, in essence, is the argument in favour of utility class frameworks. The "best practice" of separating content and styling doesn't work in the real world and, therefore, any methodology should be considered on it's other strengths and weaknesses.

HTML != CSS

The above argument, and all other arguments I have seen in favour of utility class frameworks, seems based upon an unspoken assumption: that all violations of the separation of concerns between HTML and CSS are equally bad, regardless of their direction. On the face of it, this seems like a reasonable argument; we're supposed to keep content and styling separate so, if doing things the correct way, our styling inevitably comes to closely resemble our content, why should we not do the opposite if it will give us potential benefits?

An examination of the differences between HTML and CSS, however, will reveal that the situation is a little more complicated than that.

HTML is a language for defining the content of a page. CSS is a language for styling the content of a page. We can visualise this difference.

3 variations of a set of nine buttons. On top: styled with different colours. In the middle: blank white space. On the bottom: Plain grey with default browser styling.
The bootstrap button example. On top with HTML and CSS. In the middle with CSS only. On the bottom with HTML only.

At the top of the above image we have the default bootstrap button example with both HTML and CSS loaded. In the middle we have the same example without any HTML for the buttons. On the bottom we again have the same example, but this time without loading the CSS. As you can see, CSS without content to style renders absolutely nothing; as an extension to HTML, it is entirely reliant upon the content to work. HTML without CSS, on the other hand, just looks a little… ugly and, if you are a bot or screen reader, even this limitation is effectively moot.

HTML and CSS are not of equal importance to a web page. Without content a page is useless, without styling it just looks a little dated to our current expectations and could be less user-friendly. If that is the case, why should moulding one to fit the other be considered the same? Surely, modifying your CSS to fit your HTML is a lesser sin than the other way around?

Content is King

Now, more so than ever, viewing our website on a screen is only one way in which users may interact with our content. Bots and assistive technologies have been around for almost as long as the web, but reader views, social media syndication, IoT devices, and having pages read aloud are becoming increasingly common ways in which people interact with the web. All of these care very little – if at all – about the page’s CSS. The page content, and the HTML that defines it is what is important, and modifying it to suit styling may result in a sub-optimal experience for all the users who aren’t browsing the site on the same kind of device as the developer(s) who built it.

If we are again to look at the example from before, we will see that the two violations of the separation of concerns are not equivalent. Semantic CSS’ use of descendant selectors is a less serious offence than Tailwind’s pollution of HTML with class names. After all, that’s how selectors in CSS work, they bind to HTML elements.

One argument in favour of Tailwind, however, is spot on: no website exists in a perfect world. If we were to take the importance of HTML over CSS to its logical conclusion, we would not add any classes to our HTML at all. This would result in bloated, unmaintainable CSS that needs to be updated whenever the content structure changes; the exact equivalent of the bloated, unmaintainable HTML that needs to be updated whenever styling changes produced by utility class frameworks such as Tailwind.

We should always strive to make both our HTML and CSS as independent, concise, and maintainable as possible. Just, when one of them inevitably has to be moulded to fit the other, we should try and change the styling first. After all, that’s what it’s designed to do: add styling to your existing content.

The overlapping circles of a venn diagram that we might be used to viewing the relationship between HTML and CSS as are perhaps not the best way of visualising this relationship. Something like a house might be better, with the content as the ground floor, and the styling as the first storey. Where possible we try and fit the upper floors to the base, but certain affordances must be made, such as strengthening the walls to support further floors above.

Somewhere in-between the hells of utility class frameworks and class-less HTML there is a paradise where we have to make as few compromises as possible. That paradise probably looks a little like component-based CSS. We make as few changes as possible to our HTML to give CSS room to hook into, and we re-use as much related CSS as we can. After all, it’s in our and our users’ best interests to keep both as simple as possible. Because we already have to write sub-optimal code, seems like a very poor justification for throwing out best-practices altogether.

A final word on performance

One of the advantages put forward in favour of tailwind is to do with performance. Semantic CSS, it is claimed, can get rather bloated with duplicated style declarations shared between components. In contrast to this, a utility class framework, with the proper frontend tooling, can remove all unused declarations and result in much smaller file sizes, as a small number of classes are reused over and over again. The trade off, of course, is slightly increased HTML sizes and parsing time but, overall, the total size of content + styling is smaller.

This can of course be true, but once again assumes that HTML and CSS are equivalent when they are not. CSS rarely changes and, with a proper caching strategy, can be downloaded once, then used across multiple pages. In contrast to this, HTML is by its very nature unique to each page.

Tailwind claims that, with the correct tooling and compression, pages built with it rarely require more than 10kb of CSS, even for very large sites. Personally, I have built plenty of sites using bootstrap (not a particularly performance focused framework) without extensive tooling that send less than 30kb of CSS to the user; simply by importing only the components that I am using. An approximately 300% increase in size is obviously quite a lot, but in the grand scheme of things 20kb is very little when that is cached across subsequent page loads.

This is not to condemn Tailwind and other utility class frameworks in any way. Serving less than 10kb of CSS for even very large sites is an impressive achievement, and something we should all be aiming for. I just feel that that is less to do with the framework chosen, and more to do with caring about performance in the first place. Using tailwind without optimisations will result in huge CSS files, just as will downloading the entirety of Bootstrap or Foundation; neither will ever be as performant as hand-written CSS by someone who knows what they’re doing.

In conclusion

What I hope to convey in this post is that content and styling are not of equal importance to a web page, and we should not be basing our front-end methodologies on that assumption. Utility class frameworks such as Tailwind do bring many benefits in terms of developer productivity, but those benefits must be weighed against the downsides of moving away from established best practices.

At the end of the day, the purpose of a webpage is to communicate information to users. Not every user will interact with our pages in the same way, and we should build them in such a fashion that they support as many devices and means of interaction as possible. Developer experience is, of course, important, but we should not blindly pursue it at the cost of user experience.

For now at least, I'm going to stick with semantic CSS and separating my styling and content as much as possible. You may do what you like, but I hope that after reading this you may reconsider the importance of the separation of concerns and best practices if you have already decided that they are less important than development speed.