Illustration by

Meaningful CSS: Style Like You Mean It

These days, we have a world of meaningful markup at our fingertips. HTML5 introduced a lavish new set of semantically meaningful elements and attributes, ARIA defined an entire additional platform to describe a rich internet, and microformats stepped in to provide still more standardized, nuanced concepts. It’s a golden age for rich, meaningful markup.

Article Continues Below

Yet our markup too often remains a tangle of divs, and our CSS is a morass of classes that bear little relationship to those divs. We nest div inside div inside div, and we give every div a stack of classes—but when we look in the CSS, our classes provide little insight into what we’re actually trying to define. Even when we do have semantic and meaningful markup, we end up redefining it with CSS classes that are inherently arbitrary. They have no intrinsic meaning.

We were warned about these patterns years ago:

In a site afflicted by classitis, every blessed tag breaks out in its own swollen, blotchy class. Classitis is the measles of markup, obscuring meaning as it adds needless weight to every page.

Jeffrey Zeldman, Designing with Web Standards, 1st ed.

Along the same lines, the W3C weighed in with:

CSS gives so much power to the “class” attribute, that authors could conceivably design their own “document language” based on elements with almost no associated presentation (such as DIV and SPAN in HTML) and assigning style information through the “class” attribute… Authors should avoid this practice since the structural elements of a document language often have recognized and accepted meanings and author-defined classes may not. (emphasis mine)

So why, exactly, does our CSS abuse classes so mercilessly, and why do we litter our markup with author-defined classes? Why can’t our CSS be as semantic and meaningful as our markup? Why can’t both be more semantic and meaningful, moving forward in tandem?

Building better objects#section2

A long time ago, as we emerged from the early days of CSS and began building increasingly larger sites and systems, we struggled to develop some sound conventions to wrangle our ever-growing CSS files. Out of that mess came object-oriented CSS.

Our systems for safely building complex, reusable components created a metastasizing classitis problem—to the point where our markup today is too often written in the service of our CSS, instead of the other way around. If we try to write semantic, accessible markup, we’re still forced to tack on author-defined meanings to satisfy our CSS. Both our markup and our CSS reflect a time when we could only define objects with what we had: divs and classes. When in doubt, add more of both. It was safer, especially for older browsers, so we oriented around the most generic objects we could find.

Today, we can move beyond that. We can define better objects. We can create semantic, descriptive, and meaningful CSS that understands what it is describing and is as rich and accessible as the best modern markup. We can define the elephant instead of saying things like .pillar and .waterspout.

Clearing a few things up#section3

But before we turn to defining better objects, let’s back up a bit and talk about what’s wrong with our objects today, with a little help from cartoonist Gary Larson.

Larson once drew a Far Side cartoon in which a man carries around paint and marks everything he sees. “Door” drips across his front door, “Tree” marks his tree, and his cat is clearly labelled “Cat”. Satisfied, the man says, “That should clear a few things up.”

We are all Larson’s label-happy man. We write <table class="table"> and <form class="form"> without a moment’s hesitation. Looking at Github, one can find plenty of examples of <main class="main">. But why? You can’t have more than one main element, so you already know how to reference it directly. The new elements in HTML5 are nearly a decade old now. We have no excuse for not using them well. We have no excuse for not expecting our fellow developers to know and understand them.

Why reinvent the semantic meanings already defined in the spec in our own classes? Why duplicate them, or muddy them?

An end-user may not notice or care if you stick a form class on your form element, but you should. You should care about bloating your markup and slowing down the user experience. You should care about readability. And if you’re getting paid to do this stuff, you should care about being the sort of professional who doesn’t write redundant slop. “Why should I care” was the death rattle of those advocating for table-based layouts, too.

Start semantic#section4

The first step to semantic, meaningful CSS is to start with semantic, meaningful markup. Classes are arbitrary, but HTML is not. In HTML, every element has a very specific, agreed-upon meaning, and so do its attributes. Good markup is inherently expressive, descriptive, semantic, and meaningful.

If and when the semantics of HTML5 fall short, we have ARIA, specifically designed to fill in the gaps. ARIA is too often dismissed as “just accessibility,” but really—true to its name—it’s about Accessible Rich Internet Applications. Which means it’s chock-full of expanded semantics.

For example, if you want to define a top-of-page header, you could create your own .page-header class, which would carry no real meaning. You could use a header element, but since you can have more than one header element, that’s probably not going to work. But ARIA’s [role=banner] is already there in the spec, definitively saying, “This is a top-of-page header.”

Once you have <header role="banner">, adding an extra class is simply redundant and messy. In our CSS, we know exactly what we’re talking about, with no possible ambiguity.

And it’s not just about those big top-level landmark elements, either. ARIA provides a way to semantically note small, atomic-level elements like alerts, too.

A word of caution: don’t throw ARIA roles on elements that already have the same semantics. So for example, don’t write <button role="button">, because the semantics are already present in the element itself. Instead, use [role=button] on elements that should look and behave like buttons, and style accordingly:

button,
[role=button] {
    … 
}

Anything marked as semantically matching a button will also get the same styles. By leveraging semantic markup, our CSS clearly incorporates elements based on their intended usage, not arbitrary groupings. By leveraging semantic markup, our components remain reusable. Good markup does not change from project to project.

Okay, but why?

Because:

  • If you’re writing semantic, accessible markup already, then you dramatically reduce bloat and get cleaner, leaner, and more lightweight markup. It becomes easier for humans to read and will—in most cases—be faster to load and parse. You remove your author-defined detritus and leave the browser with known elements. Every element is there for a reason and provides meaning.
  • On the other hand, if you’re currently wrangling div-and-class soup, then you score a major improvement in accessibility, because you’re now leveraging roles and markup that help assistive technologies. In addition, you standardize markup patterns, making repeating them easier and more consistent.
  • You’re strongly encouraging a consistent visual language of reusable elements. A consistent visual language is key to a satisfactory user experience, and you’ll make your designers happy as you avoid uncanny-valley situations in which elements look mostly but not completely alike, or work slightly differently. Instead, if it looks like a duck and quacks like a duck, you’re ensuring it is, in fact, a duck, rather than a rabbit.duck.
  • There’s no context-switching between CSS and HTML, because each is clearly describing what it’s doing according to a standards-based language.
  • You’ll have more consistent markup patterns, because the right way is clear and simple, and the wrong way is harder.
  • You don’t have to think of names nearly as much. Let the specs be your guide.
  • It allows you to decouple from the CSS framework du jour.

Here’s another, more interesting scenario. Typical form markup might look something like this (or worse):

<form class="form" method="POST" action=".">
	<div class="form-group">
		<label for="id-name-field">What’s Your Name</label>
		<input type="text" class="form-control text-input" name="name-field" id="id-name-field" />
	</div>
	<div class="form-group">
		<input type="submit" class="btn btn-primary" value="Enter" />
	</div>      
</form>

And then in the CSS, you’d see styles attached to all those classes. So we have a stack of classes describing that this is a form and that it has a couple of inputs in it. Then we add two classes to say that the button that submits this form is a button, and represents the primary action one can take with this form.

Common vs. optimal form markup
What you’ve been using What you could use instead Why
.form form Most of your forms will—or at least should—follow consistent design patterns. Save additional identifiers for those that don’t. Have faith in your design patterns.
.form-group form > p or fieldset > p The W3C recommends paragraph tags for wrapping form elements. This is a predictable, recommended pattern for wrapping form elements.
.form-control or .text-input [type=text] You already know it’s a text input.
.btn and .btn-primary or .text-input [type=submit] Submitting the form is inherently the primary action.
Some common vs. more optimal form markup patterns

In light of all that, here’s the new, improved markup.

<form method="POST" action=".">
	<p>
		<label for="id-name-field">What’s Your Name</label>
		<input type="text" name="name-field" id="id-name-field" />
	</p>
	<p>
		<button type="submit">Enter</button>
	</p>
</form>

The functionality is exactly the same.

Or consider this CSS. You should be able to see exactly what it’s describing and exactly what it’s doing:

[role=tab] {
	display: inline-block;
}
[role=tab][aria-selected=true] {
	background: tomato;
}

[role=tabpanel] {
	display: none;
}
[role=tabpanel][aria-expanded=true] {
	display: block;
}

Note that [aria-hidden] is more semantic than a utility .hide class, and could also be used here, but aria-expanded seems more appropriate. Neither necessarily needs to be tied to tabpanels, either.

In some cases, you’ll find no element or attribute in the spec that suits your needs. This is the exact problem that microformats and microdata were designed to solve, so you can often press them into service. Again, you’re retaining a standardized, semantic markup and having your CSS reflect that.

At first glance, it might seem like this would fail in the exact scenario that CSS naming structures were built to suit best: large projects, large teams. This is not necessarily the case. CSS class-naming patterns place rigid demands on the markup that must be followed. In other words, the CSS dictates the final HTML. The significant difference is that with a meaningful CSS technique, the styles reflect the markup rather than the other way around. One is not inherently more or less scalable. Both come with expectations.

One possible argument might be that ensuring all team members understand the correct markup patterns will be too hard. On the other hand, if there is any baseline level of knowledge we should expect of all web developers, surely that should be a solid working knowledge of HTML itself, not memorizing arcane class-naming rules. If nothing else, the patterns a team follows will be clear, established, well documented by the spec itself, and repeatable. Good markup and good CSS, reinforcing each other.

To suggest we shouldn’t write good markup and good CSS because some team members can’t understand basic HTML structures and semantics is a cop-out. Our industry can—and should—expect better. Otherwise, we’d still be building sites in tables because CSS layout is supposedly hard for inexperienced developers to understand. It’s an embarrassing argument.

Probably the hardest part of meaningful CSS is understanding when classes remain helpful and desirable. The goal is to use classes as they were intended to be used: as arbitrary groupings of elements. You’d want to create custom classes most often for a few cases:

  • When there are not existing elements, attributes, or standardized data structures you can use. In some cases, you might truly have an object that the HTML spec, ARIA, and microformats all never accounted for. It shouldn’t happen often, but it is possible. Just be sure you’re not sticking a horn on a horse when you’re defining .unicorn.
  • When you wish to arbitrarily group differing markup into one visual style. In this example, you want objects that are not the same to look like they are. In most cases, they should probably be the same, semantically, but you may have valid reasons for wanting to differentiate them.
  • You’re building it as a utility mixin.

Another concern might be building up giant stacks of selectors. In some cases, building a wrapper class might be helpful, but generally speaking, you shouldn’t have a big stack of selectors because the elements themselves are semantically different elements and should not be sharing all that many styles. The point of meaningful CSS is that you know from your CSS that that button or [role=button] applies to all buttons, but [type=submit] is always the primary action item on the form.

We have so many more powerful attributes at our disposal today that we shouldn’t need big stacks of selectors. To have them would indicate sloppy thinking about what things truly are and how they are intended to be used within the overall system.

It’s time to up our CSS game. We can remain dogmatically attached to patterns developed in a time and place we have left behind, or we can move forward with CSS and markup that correspond to defined specs and standards. We can use real objects now, instead of creating abstract representations of them. The browser support is there. The standards and references are in place. We can start today. Only habit is stopping us.

About the Author

Tim Baxter

Tim Baxter is a designer, developer, content wrangler, product manager, or strategist, depending on which hat he happens to be wearing on any given day. He can often be found poking around in the Django community, on Github, or on Twitter.

63 Reader Comments

  1. This article has annoyed me a little.
    CSS architectures should be considered again for every project. I am not bias to using any architecture. I use lots of different architectures all the time, as they solve (or reduce) different sets of problems.
    If I tried to use a WordPress theme style architecture for the application I support at work, I’d be fired.

  2. Thank you! I’ve been trying to make this case for a while myself.

    I think writing everything in your stylesheets using BEM or SMACSS (or what have you), can create as many problems as it solves. Like all things, moderation. There’s no reason to create a `.className–whichWillNeverGetUsedAgain` to bypass global scope when a `.meaningful-parent-description elementName` pattern will do.

    Global scope has a purpose and can be very useful, and a lot of these classes end up being really unsemantic and bloat the markup.

    I find that using LESS’s @import (reference) and @extend together (or postcss-reference if you prefer PostCSS) a great way to set up component patterns and then abstract them out to meaningful, and simple selectors.

  3. Not getting into the merits here. Just want to remark this:

    Tim Baxter, both in the article and in comment 20, you say W3C states paragraph tags are recommended for wrapping form elements. In fact, as pointed out by Terrence in comment 24, the section you mention is non-normative.

    And I’d like to add that you linked to an old W3C work: HTML5 Editor’s Draft from 25 October 2010. The same example can be found on the most recent HTML5 Recommendation from 28 October 2014.

    Actually, the example in the current W3C HTML Editor’s Draft uses DIVs instead of paragraphs.

  4. Using attribute selector would definitely give some possibilities in the structure of CSS.

    But… Attribute doesn’t scale in performance. If you have 1000 different classes. The SCOM/DOM parsing time won’t differ if you have 10 or 10000 elements.

    This is not true for attribute selectors unfortunately. As they on an average site, could mean the parsing could take 10-100 times longer.

  5. This is a great article and expresses ideas that I’ve been trying to convey for some time, without ever being able to put it nearly so well.

    The real beauty of the approach is that it allow CSS selector specificity to work the way it was always designed to work. Instead of treating specificity as your enemy and fighting it in the way BEM tries to do, specificity becomes your friend, a jigsaw piece that fits naturally it to its place.

  6. You can’t be serious , really !
    This approach is what i tried back when browsers started to adopt css3 and miserably failed , it can raise so many maintenance difficulties , though it’s good for small ( really small ) projects , but even then personally i think it’s overkill 😐

  7. “In HTML, every element has a very specific, agreed-upon meaning […]”

    Funniest thing I read this week 🙂

  8. I keep seeing resistance to this concept and I honestly don’t get it. When you think of atomic design and the concepts behind OOCS and BEM consistent markup that can be composed into more complex components should go hand in hand. What I see most often is OOCS/BEM being used in place of good markup – essentially admitting defeat in that A) we have to deal with crappy code so put some lipstick on the pig B) we don’t really care about architecture or optimization, we just want it to look and work a certain way.

    Why can’t this simple concept be used with a variety of other best practices? For example I’d like to investigate how this might work in a React application with SASS and CSS-Modules.

    To me form follows function. So when people get frustrated with the concept that is suggested above by pointing out how BEM classes can be used across a variety of different tags, it makes me think “yeah, you could, but why?” Do I really need “btn” styling for button, input type=”button”, a, div, span, p… and on and on, or does that “btn” class infer a behavior too and that behavior is encapsulated by a specific HTML tag or ARIA role? So why not just change the tag or the role to align to behavior and get styling with it?

    The other thing I think about is consistency. While it’s great that designers can have 34 different ways they want to style a form – do they need 34 different ways? Just like there are 1272 different ways that the developer can code that form to get to one of the 34 different designs. Is it necessary? Does it provide value?

    Sometimes constraint breeds creativity. Reducing the number of variations, constrained by how we use markup + css, may force designers and developers down a path that produces more consistency and potentially better usability. This is why there needs to be deep discussion about architecture and architectural limitations. How do you architect a solution that takes a standards approach and provides both constraints and flexibility where they are needed?

    I think there is definitely a middle ground in this religious war. I’d really like to see more openness to talking through how the challenges can be overcome versus seeing any single solution as the ultimate “already fixed that” solution.

  9. I know I’m late but this article raise an interesting (controversial) points.

    If you really believe this would be the right way to design CSS, why don’t you (or others) create some sort framework or architecture or whatever you named it that proves or demonstrate this kind a thinking in practice.

    Because currently the alternatives is bootstrap or other CSS framework that has symptom of divitis and classitis.

Got something to say?

We have turned off comments, but you can see what folks had to say before we did so.

More from ALA

I am a creative.

A List Apart founder and web design OG Zeldman ponders the moments of inspiration, the hours of plodding, and the ultimate mystery at the heart of a creative career.
Career