Utility-first CSS and CSS modules
BEM solves the problem of the shared name space with an agreement: people themselves make sure names do not collide. There are two other paths. The first is to hardly invent names at all, building the look from ready-made small classes. The second is to let build tools make names unique.
Utility classes
A utility is a class that does one thing: sets a padding, a color or a corner radius. The utility-first approach builds the look right in the markup from such classes:
<style>
.p-3 { padding: 12px; }
.rounded { border-radius: 8px; }
.border { border: 1px solid silver; }
.bg-light { background: whitesmoke; }
.bold { font-weight: bold; }
.text-green { color: seagreen; }
</style>
<div class="p-3 rounded border bg-light">
<div class="bold">Backpack</div>
<div class="text-green">€40</div>
</div>There is not a single rule about a card in the CSS — the card came out of a combination of utilities.
Tailwind
The best known utility library is Tailwind CSS. It has thousands of ready-made classes like p-4, bg-white, rounded-lg, hover:bg-gray-100, and only the ones found in the markup end up in the final file. Pros: no names to invent, the CSS barely grows, the styles are visible right in the HTML. Cons: long class attributes and a vocabulary of classes to learn.
CSS modules
CSS modules are a feature of bundlers: programs that prepare a project's code for publishing. You write ordinary short names, and the bundler makes them unique automatically — for example, .title becomes .Card_title_x7f2:
/* Card.module.css contains: .title { color: rebeccapurple; } */
import styles from './Card.module.css';
export function Card() {
return <h3 className={styles.title}>Backpack</h3>;
}The .title class from this file will never collide with another component's .title. The approach is popular in React projects.
Scoped styles in frameworks
The JavaScript frameworks Vue, Svelte and Angular can limit styles to a component: the rules from its style block apply only to its markup. In Vue this is done with the scoped attribute:
<template>
<h3 class="title">Backpack</h3>
</template>
<style scoped>
.title { color: rebeccapurple; }
</style>There are no name conflicts, and dead CSS is deleted together with the component file.
What to choose
| Situation | A fitting approach |
|---|---|
| A regular site without a framework | BEM, optionally with Sass and @layer |
| Quick prototypes, a small team | utilities or Tailwind |
| A React project | CSS modules or Tailwind |
| A Vue or Svelte project | scoped styles |
Approaches get mixed: components in BEM or in modules plus a dozen utilities like .visually-hidden. What matters is that the team agrees.
Common mistake
Copying the same long set of utilities into every place:
<style>
.px-3 { padding: 6px 12px; } .rounded { border-radius: 6px; } .bg-green { background: seagreen; }
.text-white { color: white; } .no-border { border: none; }
</style>
<button class="px-3 rounded bg-green text-white no-border">Buy</button>
<button class="px-3 rounded bg-green text-white no-border">Add to cart</button>
<button class="px-3 rounded bg-green text-white no-border">Check out</button>To change the color of all buttons, you would have to fix each one. A repeated set is moved into a framework component or an ordinary .button class.