CSS class names are public API. They appear in your HTML, in browser dev tools, and in every team member's muscle memory. The naming convention you pick affects how easy your stylesheets are to maintain — and how easy your team finds it to work together.
This guide covers the three dominant CSS naming approaches, when each works, and the underlying case convention (kebab-case) that ties them all together.
The case convention: kebab-case
CSS class names are conventionally kebab-case: lowercase with hyphens between words. .user-profile, .btn-primary, .nav-link.
The reasons:
- CSS is case-insensitive but conventions emerged early. HTML attributes (
class,id,data-*) are conventionally lowercase, and class names matched. - Hyphens work in both CSS and HTML. Unlike
_(which is sometimes parsed differently in URLs or shell commands),-is universally accepted. - kebab-case matches the broader web convention. HTML attributes use kebab-case (
data-user-id), CSS property names use kebab-case (background-color), URL paths use kebab-case. Class names follow.
Avoid:
.userProfile— camelCase reads as "JavaScript code in a CSS file".user_profile— underscores look like Python in a CSS file.USER-PROFILE— uppercase reads as shouting.User-Profile— mixed case is just inconsistent
Stick with lowercase kebab-case. Every modern CSS framework, every linter, every code review will accept this.
Approach 1: BEM (Block, Element, Modifier)
BEM was developed at Yandex and became one of the most influential CSS methodologies in the mid-2010s. It uses a strict naming pattern to encode component structure into the class name itself.
The structure:
- Block: a standalone component.
.card - Element: a part of a block, separated by two underscores.
.card__title,.card__body,.card__footer - Modifier: a variant or state, separated by two hyphens.
.card--featured,.card__title--large
Example HTML:
<article class="card card--featured">
<h2 class="card__title card__title--large">Featured Story</h2>
<div class="card__body">...</div>
<footer class="card__footer">...</footer>
</article>
Strengths:
- Explicit component boundaries — easy to grep for all of a component's CSS
- Modifiers and state changes are visually obvious in HTML
- Predictable specificity — single-class selectors throughout
- Works well with large teams because the structure is rigid
Weaknesses:
- Verbose — class names get long fast
- Awkward double-underscore and double-hyphen syntax
- Requires discipline; one developer who skips the convention pollutes the codebase
- Doesn't compose well with utility-first frameworks
BEM remains popular in design systems and in teams that prioritize readability over brevity.
Approach 2: Utility-first (Tailwind-style)
Utility-first CSS abandons semantic class names entirely. Instead, each class represents a single styling property. .flex, .text-lg, .p-4, .bg-blue-500.
Components are built by composing utilities directly in HTML:
<article class="rounded-lg border p-6 shadow-sm">
<h2 class="text-xl font-bold mb-2">Featured Story</h2>
<div class="text-gray-600">...</div>
</article>
Strengths:
- Very fast to develop — no thinking about class names
- Stylesheet stays tiny because most classes are reused
- No naming bikeshedding
- Works well with component frameworks (React, Vue) where the HTML lives inside components anyway
Weaknesses:
- HTML becomes verbose and harder to read
- Difficult to grep for "all the cards" — there's no
.cardclass - Locks you into the framework's design tokens
- Harder to maintain consistent visual rhythm if not all developers know all the utilities
Utility-first won the recent CSS popularity contest. Tailwind, in particular, has become the default for new projects in many ecosystems. If you're starting fresh in a React/Vue/Svelte project, this is a reasonable default.
Approach 3: Component-scoped (CSS Modules, styled-components, CSS-in-JS)
Modern build tools let you write CSS that's automatically scoped to a single component. The class names you write get rewritten at build time to be globally unique:
// Card.module.css
.card { border-radius: 8px; padding: 16px; }
.title { font-weight: bold; margin-bottom: 8px; }
// Card.jsx
import styles from './Card.module.css';
function Card({ title, children }) {
return (
<article className={styles.card}>
<h2 className={styles.title}>{title}</h2>
<div>{children}</div>
</article>
);
}
The build tool transforms .card into something like .Card_card__a3f9b in the output, guaranteeing no collisions.
Strengths:
- No naming collisions ever, even across hundreds of components
- Short class names within a component (no need to prefix with the component name)
- Encourages co-location of styles with components
- Works well with TypeScript (you can get autocomplete on style class names)
Weaknesses:
- Requires a build step
- Dev tools show the rewritten class names, which are harder to debug
- Doesn't help if you have shared styles across components
- Often used alongside utility-first or BEM rather than replacing them
Which to choose
The choice depends on the team and the project:
- Small project, no team: utility-first (Tailwind). Fastest to ship.
- Design system or component library: BEM. The explicit naming pays off when others consume the library.
- Large React/Vue project: CSS Modules or styled-components. Scoping prevents global stylesheet bloat.
- Server-rendered project (Rails, Django, Phoenix): BEM works well because templates aren't React components.
- Working in an existing codebase: match what's already there. Don't introduce a new convention into a code base built around an old one.
Naming patterns that work in any approach
Regardless of which methodology you pick, certain naming patterns are universally good:
- Use nouns for elements:
.card,.button,.modal. Not.cards-areaor.button-thing. - Use adjectives for variants:
.button-primary,.card-featured,.text-large. - Use state suffixes for state:
.is-active,.is-loading,.has-error. These read naturally in HTML. - Match the design system token names: if your color palette uses
primary,secondary,accent, your classes should match those names rather than introducing new vocabulary.
What to avoid
Visual descriptions: .red-button, .big-text, .left-column. These break when the design changes. If your brand color changes from red to blue, you don't want .red-button classes everywhere.
Implementation-coupled names: .float-right, .flex-thing. Tie names to purpose, not to the CSS property used.
Acronyms: .tbl, .btn-cta-pri. Just write .table, .button-primary.
Numbers: .box-1, .section-2. Numbers are meaningless. Use descriptive names.
Mixing methodologies: BEM in one file, utility-first in another, with no clear pattern for when to use which. Pick one as the default.
Converting between conventions
If you're migrating from one naming convention to another — say, refactoring an old camelCase classnames codebase into kebab-case — our bulk converter can rename a list of classes quickly. Paste the class names (one per line), switch to kebab-case mode, and copy the output.
For one-off renames, the kebab-case converter handles individual names.
The bigger principle
CSS class names are read by humans far more often than they're written. Pick a convention, document it for your team, and apply it consistently. The specific convention matters less than the consistency of its application.
The cost of mixed conventions in a stylesheet shows up in onboarding time, code review back-and-forth, and the slow growing pile of "what does this class do" questions. The fix is a one-page style guide and a linter that enforces it. The investment pays for itself in the first month.