
Since the early days of the Web, there has been tension between the ideal of "semantic HTML" and the practical reality of designing complex page layouts, which often could not be achieved without inserting style concerns into the document. More recently, frameworks like Tailwind CSS have emerged which challenge the very idea that semantic HTML is an ideal to strive for, and which commit to thoroughly embedding style concerns into HTML documents. With modern CSS features however, semantic HTML is more achievable than ever, and I do think it remains a worthy goal.
To demonstrate some modern techniques for writing semantic HTML, I've built a demo project which can be viewed at artandlogic.github.io/semantic-html-demo/. It consists of a single, unchanging HTML document, with a dropdown to select between three dramatically-different page styles. I'll be referencing it later on.

What is Semantic HTML?
Simply put, we'll define semantic HTML as HTML which
- uses relevant HTML tags to describe the meaning of page elements, and
- represents only the structure and content of the page, separate from any particular presentation of that content.
I say "we" because some might consider that this stretches the definition to include a separate concept, namely the separation of concerns between HTML (for content) and CSS (for presentation). But in my mind, effectively representing the meaning of the content also means not obfuscating it with presentational concerns, so for the purposes of this article we'll be calling it all "semantic HTML".
For example, a page's content could be defined in HTML with <div> elements:
<body>
<div class="navbar">
<div class="float-to-left">
<img class="logo">
</div>
<div class="nav">Navigation links</div>
</div>
<div class="main">Main page content</div>
<div class="footer">Page footer</div>
</body>
Or it could be defined in terms of "semantic" HTML elements:
<body>
<aside class="navbar">
<img class="logo">
<nav>Navigation links</nav>
</aside>
<main>Main page content</main>
<footer>Page footer</footer>
</body>
Each could be styled identically through CSS, so the rendered end product is the same, but the semantic HTML document encodes more information; a screen reader for example could identify the main page content and the navigation area. It also drops the <div> around the <img> tag that was only used for styling, which was cluttering up the document.
Why Semantic HTML?
First off, a semantic HTML document is one that is inherently more accessible to software, because the meaning of page elements is encoded in the document itself. This means better accessibility to screen readers, and improvements to search engine optimization.
Second, semantic HTML facilitates code reuse when building layouts that are responsive to different page sizes. Too often, I see pages which conditionally render different HTML content depending on device width; not only does this make for a less performant page, but it also creates a source of bugs, since often this creates a lot of duplication.
Finally, I believe that writing semantic HTML makes for more robust and maintanable code, since it forces the developer to really think hard about the content that exists on the page and how it all logically relates. When developers feel free to arbitrarily use <div> elements to nest content in a way that's more convenient to style, it creates pages that are brittle in the face of change. They're also harder for developers to maintain, because the HTML is difficult to navigate when developing and debugging.
If semantic HTML is so great, then I suppose it's fair to ask:
Why Isn't Semantic HTML Everywhere?
And why do frameworks like Tailwind exist which unabashedly paint it as an unattainable goal?
I think the answer is that historically, they've been right.
Up until relatively recently if you wanted to, say, have a single HTML document and style it both with a sidebar on the left for desktop and a navbar on the top for mobile, you would have had a difficult time. There was float and position: absolute for addressing these layout challenges, but the former is limited and the latter typically requires hardcoded dimensions and positions which create maintainability issues. There also haven't always been so many semantic HTML elements, leaving more to be defined with <div> tags.
Under all those limitations, it was hard to write semantic HTML documents for complex webpages that were also functional and maintainable. Since the late 2010's however, a few key CSS features found widespread browser support, which allow for completely re-arranging elements on the page: CSS Flexbox and Grid. Since then, I think it has been perfectly achievable to keep style concerns out of HTML documents and reap the benefits.
Re-arranging a page with CSS Grid and Flexbox
With these tools in hand, an HTML document can be written in a sensible structure for sequential reading (e.g. by screen reader or search engine) and then be displayed with a huge amount of flexibility. Here's a quick overview of the techniques I've found the most helpful to that end:
Reordering elements with display: flex and order
One easy trick, when displaying elements in a flexible row or column Flexbox, is that the child elements can accept an integral order property which defines their order in the row/column. And although it's a little more situational, keep in mind that flex-direction also has row-reverse and column-reverse values to reverse the order of all elements in the container.
An example use case might be a navbar which on desktop displays as a sidebar (flex-direction: column) and orders its elements "title, logo, navigation", and on mobile displays as a top bar (flex-direction: row) and orders its elements "logo, title, navigation".
Positioning elements with display: grid
This, in my opinion, is the big game-changer: CSS Grid allows elements to be positioned on a grid in a very flexible way. The grid property, while a little overwhelming when first learning it (see CSS-Tricks's Complete Guide to CSS Grid), provides a very visual way to set up the grid and assign elements to it.
In the demo application for example, the "Desktop" layout uses the following grid value:
.app-desktop {
display: grid;
grid:
"header main" auto
"navbar main" 1fr
"footer main" auto
/ 288px 1fr;
}
Without going too deep into the details, what this represents is a 2x3 grid with a header area in the top-left, a navbar area in the middle-left, a footer area in the bottom-left, and main area stretching down the whole right side. The lengths at the end of each line are the row heights, and the lengths on the last line are the column widths. With each element assigned to a grid area (e.g. with grid-area: header), they'll appear in the appropriate grid area regardless of their order in the document.
This is how it looks in Chrome with its CSS Grid debugging tools enabled:

The orange lines show the grid cells (where solid lines denote the edges of child elements and dashed lines indicate that an element is spanning across multiple cells), and the orange labels show the names of the grid areas. Despite the header, footer and navbar elements all being distinct, we can use a grid to easily style them so that they all look like a unified sidebar.
The "Mobile" layout takes the same document and places it on a totally different grid:
.app-mobile {
display: grid;
grid:
"logo header header nav" 64px
"main main main main" 1fr
"footer footer style style" auto
/ auto 1fr auto auto;
}
This is a 4x3 grid, with areas for logo, header, nav, main, footer, and style (the dropdown used to change layouts). By having four columns, the elements of the header and the footer don't need to be in alignment, allowing each to flow as expected, best illustrated in the debugging screenshot below:

These two pages have almost nothing in common layout-wise, but they share the same HTML document, and CSS Grid is doing the leg work.
Collapsing elements with display: contents
The "Mobile" layout above has one trick which is normally not allowed in CSS Grids: it puts two elements on the grid that are not direct children of the grid element:
<div class="app"> <!-- Grid parent -->
<header>...</header> <!-- Child -->
<aside class="navbar">
...
<nav class="navbar__nav">...</nav> <!-- NOT a child -->
<form class="navbar__form-options">...</form> <!-- NOT a child -->
...
</aside>
<main>...</main> <!-- Child -->
<footer>...</footer> <!-- Child -->
</div>
Thankfully, another CSS property is helpful here: display: contents. When applied to an element, it essentially makes it disappear and have its children take its place, as far as rendering is concerned. The "Mobile" layout applies this to the <aside> element, causing the <nav> and <form> elements to behave as if they were children of the <div>, and making them valid elements to place on the <div>'s Grid.
This is a great way to retain the capabilities of CSS Grid when writing semantic HTML, because elements can be defined in a sensible nested structure while still being valid targets for Grid-based styling.
CSS-based dropdowns using position: absolute and :hover
While it has limitations that mean it won't work for all scenarios, a traditional navigation menu can also be converted into a keyboard- and mobile-friendly dropdown menu purely with CSS. The demo project does this in its "Mobile" layout:

The details are a little too detailed to cover here so I'll defer to this CSS-Tricks article that covers it, but essentially all it's using are the :hover and :focus-within pseudo-classes to control when the dropdown container and mouseover effects are displayed.
Give Semantic HTML a Chance!
Tailwind and other "atomic CSS" frameworks are perfectly capable tools and many developers love them, but I can't help but feel that they don't so much solve a problem as much as they shift it elsewhere. Defining presentation inline in HTML solves the problem of unmaintainable CSS files, but it also creates a problem of duplication (whenever multiple layouts need to be implemented, at least). Some teams may be better equipped to handle the problems of atomic CSS compared to those of semantic HTML/CSS, but I'm not convinced that they're fundamentally easier to overcome.
On the contrary, the problems with atomic CSS are arguably inherent to the technique, whereas problems of semantic CSS maintainability can be addressed with largely the same techniques used for code maintainability. As challenging as it can be to maintain CSS, I think most teams would be better off addressing the root causes that are leading to their CSS maintainability issues, rather than reach for a technical solution to what is ultimately (in my view) a knowledge and process problem.
As we've seen here, styling semantic HTML is easier and more powerful than ever, and semantic HTML has a host of benefits that would be a shame to lose out on. So dig into the CSS Grid and Flexbox documentation and give it a go on your current (or next) project!