CSS Architecture: BEM, SMACSS, Utility-First and Modern Approaches
CSS does not enforce any structure. Small projects are fine with ad-hoc styles, but as codebases grow, CSS becomes a maintenance nightmare without intentional architecture. Specificity wars, naming collisions, dead code, and "I'm afraid to change this" styles are all symptoms of unarchitected CSS. This guide covers the approaches that scale.
The Problems CSS Architecture Solves
Without a system:
- Selectors become increasingly specific to override previous rules
- Class names collide across different parts of the app
- Removing old styles is risky β you cannot know what they affect
- New developers cannot tell what is safe to change
BEM (Block, Element, Modifier)
BEM is a naming convention that encodes component structure into class names.
Block: A standalone component that is meaningful on its own. Element: A part of a block that has no standalone meaning. Modifier: A flag on a block or element that changes appearance or behavior.
codeblock block__element block--modifier block__element--modifier
html<!-- Card component --> <div class="card card--featured"> <div class="card__header"> <h2 class="card__title">Getting Started</h2> <span class="card__badge card__badge--new">New</span> </div> <div class="card__body"> <p class="card__description">Lorem ipsum...</p> </div> <div class="card__footer"> <button class="card__button card__button--primary">Read More</button> <button class="card__button card__button--secondary">Save</button> </div> </div>
css/* Block */ .card { border: 1px solid var(--color-border); border-radius: var(--radius-md); background: var(--color-surface); overflow: hidden; } /* Block modifier */ .card--featured { border-color: var(--color-primary); box-shadow: 0 4px 24px rgba(0, 0, 0, 0.1); } /* Elements */ .card__header { padding: var(--spacing-md); border-bottom: 1px solid var(--color-border); display: flex; align-items: center; gap: var(--spacing-sm); } .card__title { font-size: var(--font-size-lg); font-weight: 600; margin: 0; } .card__body { padding: var(--spacing-md); } .card__footer { padding: var(--spacing-sm) var(--spacing-md); background: var(--color-surface-secondary); display: flex; gap: var(--spacing-sm); } /* Element modifier */ .card__button { padding: var(--spacing-xs) var(--spacing-sm); border-radius: var(--radius-sm); border: none; cursor: pointer; } .card__button--primary { background: var(--color-primary); color: white; } .card__button--secondary { background: transparent; border: 1px solid var(--color-border); } /* Badge */ .card__badge { font-size: var(--font-size-sm); padding: 2px 8px; border-radius: var(--radius-full); } .card__badge--new { background: var(--color-success-light); color: var(--color-success); }
BEM Benefits
- No specificity issues β all selectors are single-class, equal specificity
- Self-documenting β
.card__titletells you exactly where it belongs - Portable β blocks can be moved anywhere without style breakage
- No collisions β the block prefix namespaces all elements
BEM Gotchas
css/* Never go deeper than one element level */ .card__header__title { /* WRONG: two levels deep */ } .card__title { /* CORRECT: element of card, regardless of DOM depth */ } /* Modifiers change one thing -- do not use modifiers as themes */ .card--blue { /* WRONG: hardcoded color modifier */ } .card--highlighted { /* CORRECT: semantic modifier */ }
SMACSS (Scalable and Modular Architecture for CSS)
SMACSS categorizes CSS into five types:
code1. Base -- resets, element defaults (body, a, h1-h6) 2. Layout -- page structure (header, sidebar, footer, grid) 3. Module -- reusable components (cards, buttons, forms) 4. State -- component states (is-active, is-hidden, has-error) 5. Theme -- color schemes, fonts (rarely used separately)
css/* Base */ *, *::before, *::after { box-sizing: border-box; } body { font-family: system-ui, sans-serif; margin: 0; } a { color: var(--color-primary); } /* Layout - prefixed with l- */ .l-container { max-width: 1200px; margin: 0 auto; padding: 0 1rem; } .l-sidebar { width: 280px; flex-shrink: 0; } .l-main { flex: 1; min-width: 0; } /* Module - component names */ .card { ... } .button { ... } .nav { ... } /* State - prefixed with is- or has- */ .is-active { font-weight: 600; } .is-hidden { display: none; } .is-loading { opacity: 0.6; pointer-events: none; } .has-error { border-color: var(--color-error); }
Utility-First CSS (Tailwind Approach)
Instead of writing custom CSS, compose designs from small single-purpose utility classes:
html<!-- Tailwind: design in the HTML --> <div class="bg-white border border-gray-200 rounded-lg shadow-sm overflow-hidden"> <div class="px-6 py-4 border-b border-gray-200 flex items-center gap-3"> <h2 class="text-lg font-semibold text-gray-900 m-0">Getting Started</h2> <span class="text-xs px-2 py-1 rounded-full bg-green-100 text-green-700">New</span> </div> <div class="px-6 py-4"> <p class="text-gray-600 m-0">Lorem ipsum...</p> </div> <div class="px-4 py-3 bg-gray-50 flex gap-2"> <button class="px-3 py-1.5 bg-blue-600 text-white rounded text-sm font-medium hover:bg-blue-700"> Read More </button> <button class="px-3 py-1.5 border border-gray-300 rounded text-sm font-medium hover:bg-gray-100"> Save </button> </div> </div>
Advantages:
- No CSS file to maintain for components β styles live with markup
- No naming decisions β class names are standardized utilities
- No dead CSS β unused utilities are removed at build time (PurgeCSS/Tailwind JIT)
- Responsive variants and hover states via prefixes:
md:flex,hover:bg-blue-700
Disadvantages:
- HTML becomes verbose with many class names
- Reusable patterns require extracting components (in React/Vue) or
@apply - Requires build tooling (PostCSS)
When to use: Component-based frameworks (React, Vue, Svelte) where HTML and styles co-locate naturally.
CSS Modules
Scopes CSS to a component by automatically generating unique class names:
css/* Button.module.css */ .button { padding: 8px 16px; border-radius: 4px; border: none; cursor: pointer; } .primary { background: var(--color-primary); color: white; } .secondary { background: transparent; border: 1px solid var(--color-border); }
jsx/* Button.jsx */ import styles from "./Button.module.css"; export function Button({ variant = "primary", children, onClick }) { return ( <button className={`${styles.button} ${styles[variant]}`} onClick={onClick} > {children} </button> ); } /* Generated HTML: class="Button_button__a1b2c Button_primary__d3e4f" */ /* Class names are unique -- no collisions possible */
CSS Modules is the default in Next.js and Create React App. It eliminates naming collisions while keeping standard CSS syntax.
Choosing an Approach
| Approach | Best For | Tradeoffs |
|---|---|---|
| BEM | Plain HTML/CSS, design systems | Verbose class names |
| SMACSS | Large teams, complex apps | Requires discipline |
| Utility-first (Tailwind) | Component frameworks, fast iteration | Verbose HTML, build dependency |
| CSS Modules | React/Vue/Next.js apps | Per-file scoping only |
| CSS-in-JS (styled-components) | JS-first teams, dynamic styles | Runtime overhead, larger bundles |
Many teams combine approaches: Tailwind utilities for layout and spacing, CSS Modules or BEM for complex components, CSS custom properties for theming.
Common Interview Questions
Q: What problem does BEM solve?
BEM solves CSS specificity and naming collision problems. Without a convention, CSS selectors escalate in specificity as developers override previous rules. BEM uses single-class selectors (all equal specificity), and the block prefix namespaces all element and modifier classes, preventing collisions across components.
Q: What is the difference between utility-first CSS and writing custom CSS?
In utility-first CSS (Tailwind), you compose designs from small, predefined, single-purpose classes β flex, pt-4, text-blue-600. You rarely write new CSS. In custom CSS, you write new selectors and declarations for each component. Utility-first results in no custom CSS to maintain and no dead code; custom CSS gives more semantic, readable HTML at the cost of CSS file maintenance.
Q: What is CSS Modules and how does it prevent class name collisions?
CSS Modules transforms class names at build time by scoping them to the file β .button in Button.module.css becomes something like Button_button__a1b2c in the output. Since each component has its own unique suffix, the same class name in two different files never collides. The component imports the transformed class names as a JavaScript object.
Practice on Froquiz
CSS architecture is tested in senior frontend and full-stack interviews. Test your CSS knowledge on Froquiz β covering layout, animations, and modern CSS.
Summary
- BEM uses
block__element--modifiernaming β single-class selectors eliminate specificity wars - SMACSS categorizes CSS into Base, Layout, Module, State β works well for large traditional projects
- Utility-first (Tailwind) composes styles from small classes directly in HTML β ideal for component frameworks
- CSS Modules automatically scopes class names to files β zero collisions, standard CSS syntax
- No perfect approach exists β choose based on project type, team, and tooling
- CSS custom properties pair with any architecture for design tokens and theming
- The goal of all approaches: predictable styles, no unintended side effects, maintainable at scale