CSS Architecture Patterns

CSS at scale is hard. Global namespace; specificity battles; the dreaded `!important`. Several methodologies attempt to make CSS manageable at scale. Each has trade-offs.

This page covers the major patterns and the modern consensus.

The problem

Without methodology, CSS at scale produces:

- Style conflicts (`.button` defined in 5 files)

- Specificity wars (`.modal .button.primary` vs. `.button.primary.large`)

- Dead CSS (rules nobody uses; afraid to delete)

- Cascading dependencies (changing one rule breaks unrelated things)

The methodologies try to prevent this.

BEM (Block-Element-Modifier)

A naming convention:

```css

.card { } /* Block */

.card__title { } /* Element */

.card__title--large { } /* Modifier */

```

Names encode hierarchy. Global namespace; conflicts are visible in names.

```html

<div class="card card--featured">

<h2 class="card__title card__title--large">Title</h2>

<div class="card__body">Body</div>

</div>

```

Pros:

- Predictable

- Easy to grep

- No tooling needed

Cons:

- Verbose

- Long class names

- Discipline required

Was popular 2015-2020; less common now.

CSS Modules

Locally-scoped class names. The build tool transforms:

```css

/* Card.module.css */

.title { color: blue; }

```

```javascript

import styles from './Card.module.css';

<h2 className={styles.title}>Title</h2>

```

The build outputs unique class names per file; no global conflicts.

Pros:

- Local scoping; no naming clashes

- Standard CSS

- Build-tool magic but understood

Cons:

- File coupling (classes tied to specific files)

- Awkward for shared utilities

Common in React projects.

CSS-in-JS

Styles in JS:

```javascript

// styled-components

const Button = styled.button`

background: ${props => props.primary ? 'blue' : 'gray'};

color: white;

padding: 8px 16px;

`;

// emotion

const buttonStyle = css`

background: blue;

`;

```

Pros:

- Dynamic styles based on props

- Component-scoped

- Type-safe with TypeScript

- Theme support

Cons:

- Runtime cost (styles computed at render)

- Bundle size

- Performance issues at scale

- The "what color is this button" debugging

CSS-in-JS dominated 2018-2022 in React; some teams are moving away due to performance.

Atomic CSS / Tailwind

Single-purpose utility classes:

```html

<button class="bg-blue-500 hover:bg-blue-700 text-white px-4 py-2 rounded">

Click me

</button>

```

Each class does one thing. No custom CSS for this button.

Pros:

- No custom CSS to write

- Predictable output size (utilities are bounded)

- Fast (just CSS, no JS overhead)

- Tools (Tailwind) make it ergonomic

- Easy to delete components (no orphaned CSS)

Cons:

- HTML is verbose

- Initial learning curve

- Inline-style-feeling

Tailwind is the dominant atomic CSS implementation. Has become the modern default for many React/Vue projects.

Vanilla CSS / No methodology

Just write CSS. No conventions; rely on developer discipline.

Pros: simple; no tooling.

Cons: doesn't scale; conflicts pile up.

For tiny projects, fine. Beyond a few hundred lines, methodology helps.

The modern consensus

For most new React/Vue/Svelte projects (2024+):

- **Tailwind** for utility-first styling

- **CSS Modules** when component-specific styles needed

- **CSS variables** for theming

CSS-in-JS adoption has slowed for new projects. The performance issues and the complexity haven't justified the dynamic-styling benefits for many teams.

For design systems or component libraries, Web Components with shadow DOM provide their own isolation; can use plain CSS.

Specific guidance

Don't fight specificity

If you need `!important`, you're doing something wrong. Restructure or use a methodology that prevents specificity conflicts.

Reset / normalize

`normalize.css` or similar. Provides consistent baseline across browsers.

Modern features over old

CSS Grid, Flexbox, custom properties (CSS variables), container queries. Modern CSS is much more capable than 2010 CSS.

Performance

CSS is render-blocking. Smaller is better. Don't ship 500 KB of CSS.

Dark mode

CSS variables + media query is the modern approach:

```css

:root {

--bg: white;

--text: black;

}

@media (prefers-color-scheme: dark) {

:root {

--bg: black;

--text: white;

}

}

body {

background: var(--bg);

color: var(--text);

}

```

Common failure patterns

- **Mixing methodologies.** BEM here, CSS-in-JS there, plain CSS elsewhere — confusing.

- **CSS-in-JS for everything in performance-sensitive apps.** Slow.

- **Tailwind without configuration.** Default Tailwind is huge; configure for your project.

- **No design system.** Every component reinvents colors, spacing, typography.

- **Dead CSS.** Rules that nothing uses; afraid to delete. Use coverage tools.

Further Reading

- [ResponsiveDesignPrinciples](ResponsiveDesignPrinciples) — Layout patterns

- [WebComponents](WebComponents) — Style isolation via shadow DOM

- [ServerSideRendering](ServerSideRendering) — CSS in SSR

- [FrontendDevelopment Hub](FrontendDevelopmentHub) — Cluster index