Skip to content

The Three Laws of Utility Classes

If ya gotta use ’em, use ’em. But don’t let them break the world.

By Jared White

I’m starting to come around on utility classes.

But not in the way you think.

I have many complaints with utility classes—and in particular the framework which helped popularize them to the masses: Tailwind. I have been rather vocal in my criticism of Tailwind over the past couple of years, and I will continue to speak up. I don’t believe the creators of Tailwind—talented though they may be—are interested in engaging in good faith dialog with their critics. I believe they oversimplify and thus dismiss the many valid critiques of “utility class all the things!!”—and even worse, they use outdated and poorly-sourced examples of the so-called “best practices” they’re striving against and “fixing” with their strangely-opinionated approach. And beware the Twitter mob which comes after you if you dare say anything negative about their pet framework / inventor. Yikes.

Still, I am willing to concede that the generic concept of utility classes (sometimes referred to as atomic classes) can come in handy, especially when prototyping new stuff or sharing little reusable code snippets.

So rather than wave my hands emphatically and tell you “never ever use utility classes, damnit!”, I will instead offer some suggestions on how you might go about using utility classes without breaking the world in the process. I call these ideas “laws” in a very tongue-in-cheek way. (I don’t believe in programming laws.) Think of it more like the Pirate Code…more guidelines than rules!

So here we go: The Three Laws of Utility Classes.

Law #1: Utility Classes Consume the Design System, They Don’t Create It #

Explanation: Your design system should live independently of your utilities, and be fully usable outside of your utilities. Any 1:1 coupling between your design system and utility classes should be discouraged.

Here’s what I mean by that. Look at an output stylesheet generated by Tailwind. You might see something like this:

.mb-4 {
  margin-bottom: 1rem;
}

Uh oh! This has created a 1:1 coupling between the design system and this particular utility class called mb-4. Why does the 4th size in an ephemeral sizing scale (ephemeral because nothing in the built artifact of the website has independently established this sizing scale) equal 1rem? How would you use this 4th size of 1rem anywhere else apart from this utility class or similar utility classes like it? If you wanted to change the design system at runtime to aid in prototyping or testing—say, try a 4th size as 1.15rem instead of 1rem—how would you? (Newsflash: you can’t!)

Resolution: Instead, this class should consume an actual CSS-based design system.

.mb-4 {
  margin-bottom: var(--size-4);
}

Aha! Now the design system and the utility class have become decoupled. You can continue to use mb-4…or you can use style="margin-bottom: var(--size-4)"…or you can use element.style.marginBottom = "var(--size-4)" in JavaScript…or you can use .any-selector { margin-bottom: var(--size-4) } in an external stylesheet. You can easily look up the value of the --size-4 CSS variable. You can change the value of the --size-4 variable at runtime and the entire page will redraw to reflect that change. You can redefine the --size-4 variable selectively within any particular DOM tree. Definition and usage of the --size-4 variable is independent of whichever tool was used to generate the utility classes, thus eliminating vendor lock-in and ensuring future portability. Heck, if you’re relying on third-party design tokens you might decide to switch systems outright and then you can redefine --size-4 to another variable coming from another design system! (What?!)

CSS variables are extraordinarily powerful, extremely portable, and 100% future-proof. It’s quickly becoming inconceivable that a modern design system on the web wouldn’t be built from the ground up to use them.

Law #2: Utility Classes Cover Only the Low-Hanging Fruit #

Explanation: Utilities can cover the vast majority of typical use cases for styling elements (typography, colors, borders, basic grid/flex layout, etc.) They should not cover the entire range of styling options available in CSS.

While not all utility class frameworks attempt to essentially replicate virtually all CSS properties and thus eliminate stylesheet authoring outright, this is increasingly the expectation of web developers who have wholeheartedly embraced utility classes.

This has lead to an almost absurdist state of affairs where you can find a utility class for the most obscure corners of CSS. For example, Tailwind provides a utility for…wait for it…applying a sepia tone filter on an image, called sepia.

Really? You’ve actually gone there? Is style="filter: sepia(100%)" too hard to type out or something? Oh, but you don’t understand…I want it to change the filter when I hover and then on this breakpoint I want it to add this other filter and then I…

Stop. Just. Stop.

I’m telling you right now: you’re almost never, ever, ever going to do all that. And if you actually do decide to implement that on a production website which hopefully has undergone rigorous vetting for UX and accessibility considerations (speaking personally, I am quite bothered by images which blur/unblur or change color on scroll or hover or whatnot…it’s quite hard on my eyes and extremely distracting), that behavior should be encapsulated within a proper image component…which means you could have written a proper stylesheet for that image component to handle responsive/hover cases of image filtering. 😅

But wait, there’s more!

Tailwind also provides a backdrop-sepia utility class! For all those times when you wish the content behind your transparent overlay was displayed in sepia tone! 🤡

OK, now you’re just fucking with us. This has got to be a troll right? RIGHT?!?! (And I haven’t even gotten to the “not actually utility classes at all” bizarro-world that is arbitrary properties… 🤦🏻‍♂️)

Resolution: Get disciplined about the number of utility classes you expect to define, teach, support, and actually use on a daily basis. You probably don’t need very many—and the more you apparently need, the more it likely reveals something problematic about your development process.

Which leads me to the third and final law:

Law #3: Utility Classes Complement, not Supplant, Semantic CSS #

I’m not sure how emphatically I can endeavor to state this, so I will attempt to use a large, bold font in all caps with a highlighter to make sure you can fully appreciate the force with which I am attempting to convey this message:

UTILITY CLASSES ARE NOT A VALID REPLACEMENT FOR WRITING STYLESHEETS!

Whoa there, friend…before you crack your knuckles and prepare to write a lengthy screed to inform me all the reasons why utility classes can, in fact, replace writing vanilla CSS and all the ways you or your team have done this and why it’s so much better and how the performance is so much better (nope) and why everyone everywhere should do it and if it’s good enough for GitHub and Sarah Dayan and 39% of some trendy survey or whoever it should be good enough for you, blah blah…well, let me just stop you right there and tell you:

I have heard all this before. Many times. Oh so many times. You can all avoid the trouble to write me now. 😆

Here’s the deal: if I’m willing to grant that maybe—just maybe!—disciplined use of utility classes might in fact be a handy-dandy tool in the ol’ web developer tool-belt after all, then you must be willing to hear me out.

My thesis is this:

Semantic CSS is now—and throughout our present lifetimes will continue to be—the most compatible, most extensible, most maintainable, most future-proof, most widely-used, most up-to-date, and most vendor-neutral manner of applying multimodal presentation & design to structured web content.

In addition, I believe it should be a given in our industry that one must advocate for learning and applying detailed knowledge of “vanilla” CSS as a core web technology. The collective wisdom born from several decades of innovation done within a standards-based process involving a myriad of companies and individual creators alike spanning the globe should at least be respected, if not appreciated.

(If you don’t yet fully appreciate the power of semantics when it comes to everything in web development, there’s actually a pretty good article over on MDN about semantics within all three core web technologies: HTML, CSS, & JavaScript.)

All that being said, since it’s apparently not axiomatic in the web development industry that one should aspire to use core web technologies in the manner in which they were intended by their authors, I will break down all of the points I made above:

Most Compatible: You can jump from project to project, company to company, career to career…and, amazingly, that vanilla code in that vanilla stylesheet will continue to function. Anywhere. Everywhere. Vanilla, semantic CSS is quite literally supported by every browser, every stack, and every editor. There is no framework which can claim such ubiquity.

Most Extensible: If you pick a very specific type of CSS framework—particularly one based around utility classes—you are locked into that framework’s way of doing things. You aren’t afforded the ability to try out farther-flung design examples, other component libraries, other frameworks, other tools. If you instead build a system mainly out of vanilla, semantic CSS, you can dip in and out of a wide variety of tools and patterns painlessly.

Most Maintainable: When you use a CSS framework with an elaborate build system, you are forced to use that build system and keep it updated and working correctly, and your ability to test or make changes outside of its particular build process is limited (as demonstrated earlier on in this article). Vanilla, semantic CSS requires little to nothing in the way of a build system and is easily changed and tested directly within the browser itself.

Most Future-Proof: Frameworks come, frameworks go. I guarantee your pet framework which is popular now will not be hot stuff decades from now. Yet the vanilla CSS you learn and use now will still be entirely relevant decades from now. How do I know this? Because much of the vanilla CSS I used and learned decades ago is still relevant today!

Most Widely-Used: Despite protestations to the contrary, no CSS framework has ever reached ubiquity. There remains a wide variety of frameworks and tools out there, with more arriving on the scene every day. Just Tailwind alone now has a number of competitors which are in many ways very similar! The one common denominator between all of them is, when worse comes to worst, you can just knock out some regular ol’ CSS. It’s the escape hatch. Everyone can learn it. Everyone can use it. And if you need to reconfigure or contribute to any CSS framework—yes, even Tailwind—you yourself must know vanilla CSS. You can’t stay abstracted for too long.

Most Up-to-Date: The pace of innovation in the CSS specs has actually increased dramatically, with powerful new features landing in evergreen browsers all the time now. It’s truly an astonishing state of affairs. (I’ve been around long enough to well remember the terrible, awful days of IE6!) You know which flavor of styling is guaranteed to support every single one of these innovations the millisecond it arrives in web browsers? Vanilla, semantic CSS. You don’t need to wait for a build tool, a framework, a component library, or anyone or anything else to use a new feature. You just use it. It’s that simple.

Most Vendor-Neutral: Arguably this is the most important point to me. CSS frameworks—and Tailwind in particular—have become the “Bates Motel” of web development. Guests check in but they don’t check out! Once you’re in, you’re IN. There’s no escaping it. Decide on a different strategy for your styling? Need to integrate cleanly with some other tools/libraries/frameworks as well? Previously used some utility classes for prototyping but now you’re ready to build the real thing? Well tough toodles for you, you’re stuck with that build process and will need to maintain it indefinitely. Wah wah wah.

Or…Maybe Not Anymore! Introducing: Vanilla Breeze 😎 #

This has got to be the worst infomercial you’ve ever read in your life. 😂

All kidding aside, I have actually built a website and open source tool called Vanilla Breeze which will take a chunk of HTML you paste in filled with Tailwind utility classes, and it will automatically strip out Tailwind classes from that HTML and generate a companion stylesheet of vanilla, (mostly) semantic selectors/properties alongside CSS variable-based design tokens compiled from Tailwind’s own design system. In most cases, the output will appear identical, yet you will have code that is future-proof, portable, and ready to customize and import into your project using any build system or none at all. 🤯

In addition, I fully expect to add a way to “emit” a web component with shadow DOM-based styling, something that is very difficult to accomplish with regular Tailwind classes. Imagine, if you will, a world where you can very quickly prototype a new component using utility classes—then with the push of a button, transform your HTML prototype into a 100% vanilla web component which looks virtually identical.

Cool, right?

The true irony of this is it might actually make me like Tailwind again. 🤪

The whole @apply concept has always seemed like a hack to me, and while I appreciate that it exists despite the creator of Tailwind’s apparent hatred of the technique (Vanilla Breeze uses @apply liberally under the hood!), I think it’s a far, far better solution to recalibrate Tailwind’s output so that the design system is decoupled from utility classes and you can quickly jump from HTML class soup to a proper “buildless” markup + stylesheet combo.

Thus Tailwind morphs into a sort of experimental toolkit for building and designing stuff in the early stages of a project, or a page, or a component. Thankfully with no more vendor lock-in, no viral takeover, no weird hatred of separate stylesheets (I will never in my life understand this), and no incompatibility with buildless systems or third-party CSS frameworks and tooling.

Conclusion: Utility Classes are Great…Until They Aren’t, So You Should Be Able to Eliminate Them Easily and With Finality. #

The bottom line of this very long-winded essay is that utility classes are a tool. A tool like so many other tools we have. Used judiciously, in moderation, with full understanding of their shortcomings, and with the ability to strip them out at a moment’s notice, they can be a handy tool to have. But let them virally take over your project, your company, the industry—they end up destroying the very thing they originally sought to cultivate: making it easy and fun to build web pages out of simple building blocks without maintenance nightmares and mystifying code-fu.

I hope new tools like Vanilla Breeze, as well as utility classes which consume rather than define a CSS design system (Nord Health’s utility classes are a great demonstration of this), can showcase what’s possible when you combine the best aspects of both vanilla, semantic CSS and utility classes—and why it’s important to seek out and utilize tools which specifically don’t encourage vendor lock-in and a “Bates Motel” ethos of web development.

Want to join a fabulous community of web developers learning how to use “vanilla” web specs like HTTP, HTML, CSS, JavaScript, & Web Components—plus no-nonsense libraries & tools which promote developer happiness and avoid vendor lock-in?

Join The Spicy Web Discord

It’s entirely free to get started. And we’ll soon be launching paid courses to take you even deeper down the rabbit hole, so stay tuned! Vanilla has never tasted so hot.