Skip to content
Courses CSS Nouveau Vanilla Has Never Tasted So Hot Class-Less CSS FTW!

Class-Less CSS FTW!

Why class= might just be the most abused (and least useful) aspect of style authoring today #

I’ll let you in on a little secret.

I kinda wish the class attribute didn’t exist.

Can there be any more over-used and ill-used examples of markup in the history of the web than <div class="…"> and <span class="...">? They are the least semantic tags in all of HTML, and when they’re riddled with dozens and dozens of “inside baseball” classes which don’t properly describe purpose or function, it quickly descends into madness.

Like, what in the name of all that is holy is this even supposed to mean:

<div class="mt-12 m-auto -space-y-4 items-center justify-center md:flex md:space-y-0 md:-space-x-4 xl:w-10/12">
  <div class="relative z-10 -mx-4 group md:w-6/12 md:mx-0 lg:w-5/12">
    Sup dawg.
  </div>
</div>

This is incomprehensible jargon. Look at this gist where I took an example “component” and removed all the content but left the markup. You have no idea what this even is or what it does. None.

This class of classes (heh) represents a “write-only” sort of mentality. Try coming back to code like this six months later, or try explaining it to someone unfamiliar with the exact terms involved. Good luck with that!

In my article The Three Laws of Utility Classes, I try to establish some guardrails around utility classes in the cases where you feel truly compelled to use some non-semantic class names in your markup. But I continue to stress that utility classes should be a last resort. Or perhaps a first resort if you know you’re just writing a prototype and you fully intend to refactor it later. That’s fine! We all write a bunch of junk code just to make the darn thing work, and then come back and fix it later (right? RIGHT??). Unfortunately, with utility classes run amuck, the fixes never arrive.

So what’s the solution then? Try to come up with lots of semantic class names to map to all your elements? Write a bunch of stylesheets filled with semantic class names? Is that really the answer?

Bugs Bunny says no.

In this episode and beyond, I will show you how to accomplish 90%+ of all your styling needs without relying on class. At all. 😱

Yes it is possible. All of it.

Han Solo says All of it

Why Go Class-less? #

The rise of classless themes has been quite a welcome shift in thinking around how to write HTML. In a few cases they perhaps push the boundaries a little too far, or make too many assumptions about how a standard HTML element should get used. But I think it’s a great place to start.

It’s helpful to talk about classless themes by contrasting that with a polar opposite approach: a “hard” CSS reset. The no-styling-without-classes approach—essentially stripping default styles from all HTML elements. So if you wrote HTML like this:

<section>
  <article>
    <header>
      <hgroup>
        <h1>Title</h1>
        <p>Subtitle</p>
      </hgroup>
    </header>

    <p>This is the best article you've ever read.</p>

    <ol>
      <li>It's short.</li>
      <li>It's succinct.</li>
      <li>It has a list in it.</li>
    </ol>

    <blockquote>
      <p>Critics say there's nothing like it!</p>
    </blockquote>

    <p>And now you know why.</p>

    <footer>
      <p>Written by Yours Truly</p>
    </footer>
  </article>
</section>

You would literally just get several lines of unstyled text and that’s it. Nothing would look like a heading, or a list, or a blockquote, or anything. It would just look like you typed text into a blank page in a word processor and that’s it. Any and all formatting would have to be applied by adding classes and writing a stylesheet against those class names.

Yuck!

The thing that can make styling hard isn’t that there’s styling. It’s that there’s ugly styling, or hard-to-override styling. Maybe in the past we had to deal with “specificity wars” around some of this stuff, but techniques like :where and perhaps even Cascade Layers can help us route around problems. Thinking that HTML elements having default styling will make your life so much harder is missing the forest for the trees.

Some folks have traditionally offered the solution of supplying a container class for applying default styles. Like if you wrap everything in <div class="content"> or <div class="prose"> or whatever, then you’re golden.

But why? Why not go the opposite direction?

Let’s have “default” styles apply to everything, and then use something like <div class="reset"> if you really need a big block of markup to get stripped down.

Better yet, just write components—and you could really leverage the shadow DOM to full effect here—which encapsulate the level of reset you need without affecting the rest of the site. Globally shortchanging built-in HTML elements simply isn’t necessary in this day and age. Don’t want it. Don’t need it.

Classless Themes #

The wonderful thing about going classless is you don’t have to start from scratch. There is plenty of good prior art to choose from. Just a few of my favorites:

Pico #

I used this just recently to help me get a leg up fast putting together the Intuitive+ website, and I was quite pleased. At 10K stars on GitHub, this is a popular option.

Tiny.css #

This is new entry to the scene…as it says on the tin: “Tiny.css is a very minimalistic css library to override the horrendous default style with good looking design. If you quickly want to get an html page up and running, tiny.css is for you!”

MVP.css #

I’ve also used MVP in the past. It serves as a rather nice launching pad and is “designed to look great on all browsers and devices out of the box for rapid prototyping.”

Simple.css #

Simple.css provides a pretty comprehensive default site theme you can use for rapid styling and even offers Eleventy and Astro starter templates.

Open Props “Normalize” #

I’ve talked about Open Props before in this series—a fantastic project. I could quibble a bit with calling this a “normalize” stylesheet, but hey.

The nice thing about pulling this one in is that you get to work with all of the awesome Open Props design tokens to build out additional functionality on your project.

My main piece of advice if you decide to reach for a classless CSS theme is to copy what you need out of it and tweak accordingly. I wouldn’t recommend simply “linking” to it like it’s a dependency. It’s supposed to be a starter kit. Pull in the bits you like. Discard what you don’t. Use it as the basis of your own theme.

Thinking in Classless #

When thinking in classless, you’re constantly trying to find ways to leverage semantic HTML tags and DOM structure. For example, instead of styling header.page-header, maybe you just style body > header. Instead of styling img.featured-article-image you style article figure > img. Instead of styling input.input, you just style input (or more accurately, specific types of inputs).

Wild, I know!

You’ll get pushback though. People will tell you that DOM structure is “brittle” and the relationship between parent and child elements may change all the time. They’ll claim you must use carefully named classes for every teeny-tiny little part of a block of markup, because otherwise your styles will break due to specificity issues.

Don’t listen to them.

As you’ll learn throughout this course, we’re paying extra-close attention to managing DOM structure well and keeping style selectors “scoped” to well-regarded semantic element combinations or component/component-like blocks. It’s virtually a given that if you edit one of these scopes, you’ll need to modify both the HTML & CSS as a cohesive unit.

Buy hey. Don’t feel like styling article figure > img anymore but instead wanna go with .article__image--featured? Cool. Just don’t do it as a default. Because it’s a bad default.

Styling Structure, Not Elements in Isolation #

So let’s talk about how you could go about theming various parts of your page layout and UI without using classes. The first principle to keep in mind is that, as much as possible, you are styling the structure of your content. Assuming your HTML is well-written and semantic, you can use that to your advantage in all sorts of ways.

Here’s an example. And it’s actually a very recent update to HTML. How exciting!

So many examples of content on the web—primarily articles, blog posts, etc., but you can find it on page headers or sections of all sorts—have a title and a subtitle grouping. For years, many of us have been using hn tags for this. If h1 is a title, h2 is a subtitle. Or if h2 is a title, h3 is a subtitle.

There are two huge problems with this approach.

  1. The heading levels in HTML convey special semantic meaning: they provide key insight into the outline of an HTML document. A lower heading level after a higher heading level means that the content following lives in a new “indentation” of the outline. A title/subtitle grouping really shouldn’t create a new indentation, as the subtitle isn’t a standalone content heading but simply reinforces the title.
  2. It’s really hard to maintain consistent styling if you’re constantly dealing with the same content represented by various heading levels depending on the context. For example, on an article page you probably want the article title to be an h1. But for a reference to that article on an archive page, you’d want the title to be an h2 or even an h3. That may look OK if you’re just talking titles, but what about subtitles? Now you have to manage pairs of h1+h2, h2+h3, h3+h4it’s a nightmare.

Thankfully, there is now an officially sanctioned solutionyears in the making! 😂 It’s to use the <hgroup> tag.

The <hgroup> tag was originally added to HTML5, and then we thought it was deprecated and on its way out, but it’s been newly revitalized. And as it turns out, you really don’t need to use a heading tag for a subtitle at all. This is wholly sufficient:

<header>
  <hgroup>
    <h1>I'm a Page Title</h1>
    <p>I'm the subtitle.</p>
  </hgroup>
</header>

<main>
  <article>
    <header>
      <hgroup>
        <h2>I'm an Article Title</h2>
        <p>I'm the subtitle for this article.</p>
      </hgroup>
    </header>
  </article>
</main>

This is all documented in detail by HTML spec contributor Steven Faulkner here.

My point in mentioning all this is because now that there is a proper structure in place for title/subtitle groupings, you can style the structure:

hgroup > :where(h1,h2) {
  font-family: var(--title-font-family);
  font-size: var(--title-font-size);
  color: var(--title-color);
  margin-block-end: var(--size-2);
}

hgroup > h2 {
  font-size: calc(var(--title-font-size) * 0.8);
}

hgroup > p {
  font-family: var(--subtitle-font-family);
  font-size: var(--subtitle-font-size);
  color: var(--subtitle-color);
}

In the past I might have had to use classes like <h1 class="title">, <h2 class="subtitle">, etc. No longer!

Let’s look at the example I talked about previously where you want to style the featured image for an article. Instead of:

<img class="article-image" src="..." alt="..." />

We could go a little further and use our dear friend the <figure> element:

<figure>
  <img src="..." alt="..." />
</figure>

The nice thing about this is if you decide you want to add a caption above or below the image later on, that’s easy to do!

<figure>
  <img src="/images/the-beetles.jpg" alt="members of The Beatles walking across the street near Abbey Road Studios" />
  <figcaption>Lennon, Starr, McCartney, Harrison</figcaption>
</figure>

And now we have a structure to style!

article figure {
  margin-inline: var(--size-2);
  margin-block: var(--size-8);
  background: var(--gray-1);
}

article figure > img {
  width: 100%;
  box-shadow: var(--shadow-3);
  border-radius: 3px;
}

article figure > figcaption {
  font-size: var(--font-size-1);
  font-weight: 600;
}

There are many, many examples of this. And this approach can also work wonders once you introduce custom elements. I’ll be talking a lot more about custom elements as well as web components later on, but suffice it to say they provide the ability for you to create your own structures!

Custom Elements for Enhanced Structure #

I often like to employ the technique of naming custom elements after existing elements, but with additional structural terminology. For example:

<header>
  <header-logo></header-logo>
  <nav>
    <nav-social-links>...</nav-social-links>
  <nav>
</header>

or

<main>
  <main-content>
    <article>
      <article-content>
      </article-content>
    </article>
  </main-content>
</main>

Obviously in the examples above, <nav>, <main>, and <article> are built-in HTML elements. But I’ve surrounded them with additional structure using custom elements. The article has an <article-content> child, and itself lives in <main-content>. Isn’t that so much nicer than this?

<div class="main">
  <div class="main-inner">
    <div class="article">
      <div class="article-content prose">
      </div>
    </div>
  </div>
</div>

or even worse:

<div class="3g48yhKa">
  <div class="hiq48oJ4">
    <div class="fiH4if8s">
      <div class="a3hgu83h">
      </div>
    </div>
  </div>
</div>

So many divs! So many incomprehensible class names! 😭

I’ll be talking a lot more about “div soup” in the next episode.

Style Variations Using States and Queries #

The next principle I’d like to cover is the idea of styling “variations” of elements using concepts such as states and content shapes. States are a concept a little foreign to many of us who have been writing HTML for decades, but more and more we are seeing HTML evolve into a language which can convey “live data states” as part of its core structure.

Accessibility (A11Y) is a major topic outside the scope of this course, but for a rapid-fire overview of the topic you can listen to Episode 8 of my podcast Just a Spec. What makes this relevant and interesting to us as we try styling parts of a page template or component is…well, let’s have MDN explain it to us:

ARIA attributes enable modifying an element’s states and properties as defined in the accessibility tree.

For example, say you have a “breadcrumb” navigation showing links for the various levels. The current level should be highlighted or displayed differently from the other links in the navigation. Instead of adding class="current-page" or some other invented attribute to indicate that link is the current page, you can instead reach for aria-current="page". You really should anyway because that’s what’s good for accessibility, and the bonus is you can style that directly!

site-breadcrumb > nav a {
  /* typical breadcrumb styling */
}

site-breadcrumb > nav a[aria-current="page"] {
  /* style the current page */
}

There are many such ARIA properties or states available to you. aria-selected is another great one: ideal for tabs, focusable cells or rows in grids, and some other common structural contexts.

ui-tab {
  /* default styles for tabs */
}

ui-tab[aria-selected="true"] {
  /* selected tab styles */
}

Again, no special classes required!

✨ Live Demo ✨ #

All of these ARIA attributes are well documented on MDN and can replace the usage of classes for styling in a number of different cases.

In situations where an ARIA attribute doesn’t apply to your particular need, you can fall back to classes, sure. Or…you could still just use attributes!

<img src="..." alt="..." class="is-hero" />
<!-- vs. -->
<img src="..." alt="..." hero />

The difference between styling the two is:

img.is-hero {

}
/* vs. */
img[hero] {

}

Seriously, it’s six of one, half-dozen of the other. Specificity is identical. And the dedicated attribute is actually superior to a class because it can be more than just a boolean, it can contain any value.

Don’t like using arbitrary attributes on built-in elements? Use data-*! For example:

<img src="..." alt="..." data-variant="hero" />
img[data-variant="hero"] {
  /* hero image variant */
}

We’ll go into this stuff more in-depth in Episodes 6 and 7.

Finally, there’s no shame in styling tags based on id if they’re clearly unique structural elements of a page template. For example, instead of aside.site-sidebar, why not aside#site-sidebar? Is your site going to have multiple sidebars?!

Beyond Responsive Design #

Now let’s talk about what I call “content shapes”. This is tightly coupled with the concept of Intrinsic Web Design that’s an evolution of Responsive Design.

Responsive Design is a way of affecting the styling of your page based on outside factors (screen size, possibly orientation, light/dark mode, and other such globally-scoped media queries).

Intrinsic Web Design is a way of affecting the styling of various template parts and components based on what’s inside of them, or what they’re inside of, or what’s around them, or the amount of space they happen to occupy.

It used to be really hard to do any of this with standard CSS. For example, let’s say I want to change how “rounded” the corners of a box is based on how big the box is. If the box is big, make them very rounded. If the box is small, make them only a little rounded.

You might be tempted to use a media query for this:

my-nifty-box {
  border-radius: var(--really-big-ass-corner-radius);
}

@media (max-size: 600px) {
  my-nifty-box {
    border-radius: var(--teeny-tiny-corner-radius);
  }
}

OK, that works! Until it doesn’t. Because you just put your box over in a sidebar where there isn’t much space even on a large display, and it looks goofy to have large rounded corners on the small box. What you really want is the shape of the parent content to affect the styling of this content. What you need is a container query.

We can add use this query in place of a media query, but first we need to set up a containment context. We can use the sidebar itself for this purpose:

aside#sidebar {
  container-type: inline-size;
}

And then use a container query for the box:

@container (max-size: 600px) {
  my-nifty-box {
    border-radius: var(--teeny-tiny-corner-radius);
  }
}

Now we’re saying: instead of the box looking at the screen size to figure out its styling, the box is looking at the shape of the page region it finds itself in.

I Can :has Cheezburger? #

Big cat No you didn't GIF

The new :has selector, also dubbed the “parent selector” but more accurately the “family selector”, operates on a similar principle but in reverse. It’s saying “style ME based on what’s going on over THERE” where over there is usually children but can also be siblings.

Check out this example where I can essentially style first, last, and middle list items without ever resorting to first/last child/of-type pseudoclasses.

✨ Live Demo ✨ #

But let’s be honest—the parent selector usage is probably the most useful. And boy is it useful! Check out this amazing example:

figure:has(> figcaption) { ... }

This lets you apply styling to figure tags but only if they are captioned. This was impossible to do before :has if you can believe it. We truly live in exciting times.

You can stack multiple :has on top of each other. This selects any article tag which has both h1 and h2 direct descendants:

article:has(> h1):has(> h2) { ... }

And wow, we can finally style entire form fields based on validation errors!

label {
  color: var(--field-color);
}

input {
  border: 2px solid var(--field-color);
}

form-field:has(:invalid) {
  --field-color: var(--color-invalid);
}

Examples like these and much more besides are all shown off in this excellent Chrome Developer Blog article. I feel like I’ve only tried like 1% of the possibilities out there already…

Our Old Pal style= #

Forget everything you ever knew about “inline styles” and why they’re bad and why you shouldn’t use style=.

In the era of custom properties, alongside logic like calc and clamp, it’s irrelevant.

The reason style="margin-top: 27px" was bad wasn’t because of “inline styles” vs. “write a stylesheet”, it was bad because:

“Obviously” the solution is to use external stylesheets and name things. Except…I think that ignores the very real need for one-offs and tweaks. Heck, the popularity of utility classes showcases this, and they have to work overtime to convince people it’s not just “inline styles” all over again. (I mean, it really is, despite their protestations…)

So how do we use style= for one-offs and tweaks without resorting to magic numbers and semantically meaningless values?

CSS Custom Properties. 😎

Like we covered in the previous episode, we can utilize the design tokens, the variables of our design system, in our style attributes. Not only does this avoid the magic number problem, it also conveys semantic intent! Consider this:

<section style="
  margin-block: var(--content-spacing-sm);
  padding-block: var(--content-padding-sm);
">
...
</section>

<section style="
  margin-block: var(--content-spacing-lg);
  padding-block: var(--content-padding-lg);
">
...
</section>

These two sections have different margin and padding, supplied by the variables in their style attributes. No magic, extreme clarity, and—perhaps best of all—these variables could also be responsive. (Perhaps on small screens, large and small sections have the same margin and padding!)

And of course, you have full access to calc and clamp in case you want to add some additional logic (as we learned in the previous episode).

Wrapping Up #

There are so many techniques available to us to style layouts, elements, and structural relationships in the DOM without ever reaching for class=.

It kind of makes you wonder: if there are so many great solutions we can reach for, why use class= at all?

Exactly.

But if this is the case, why do we continually find “div tag soup” splattered all over the web? Why do we “view source” many popular websites only to find some of the most horrendous markup ever encountered by mortal man? Is this simply inevitable, a by-product of “modern web development” and the needs of engineering teams? Or are we in desperate need of a hard reboot, a fresh look at everything about HTML & CSS starting with first principles.

Yeah, that. 😂

Next Episode:
Div Tag Soup Begone