CSS Architecture for Large Projects

A 500-line CSS file is manageable. A 50,000-line CSS file is a liability. Every large frontend project eventually reaches a point where adding a new style breaks something unexpected, where nobody dares delete old rules, and where the cascade feels more like chaos than a feature.

CSS architecture is about preventing that outcome. It is the set of conventions, file structures, and methodologies that keep your styles predictable, maintainable, and performant as your project grows. Having worked on projects where CSS grew from a few hundred lines to tens of thousands, I can say with confidence that the teams who invest in architecture early save themselves months of pain later. On one project I inherited, the CSS bundle had reached 380KB uncompressed, with a specificity graph that looked like a heartbeat monitor. It took the team three months of dedicated refactoring to bring it back under control.

Why CSS Needs Architecture

CSS is global by default. Every rule you write can potentially affect every element on the page. In a small project, this is fine. In a project with dozens of developers and hundreds of components, it becomes a serious problem.

Without architecture, teams fall into predictable patterns. New styles are added at the bottom of the file. Specificity increases over time as developers use more targeted selectors to override existing rules. Dead CSS accumulates because nobody can confidently determine which rules are still in use. Eventually, the team agrees that the CSS “needs a rewrite,” and the cycle begins again.

According to research from HTTP Archive’s State of CSS report ↗, the median CSS weight on the web has grown steadily year on year, with the top 10% of sites shipping over 800KB of CSS. Architecture breaks this cycle by providing structure, naming conventions, and clear ownership of styles. Enforcing these conventions with automated linters and formatters is what makes them stick long-term.

Naming Conventions: BEM

BEM (Block, Element, Modifier) is the most widely adopted CSS naming convention, and for good reason. It creates a direct, readable relationship between your HTML and your CSS.

/* Block */
.card { }

/* Element (part of the block) */
.card__title { }
.card__body { }
.card__footer { }

/* Modifier (variation of a block or element) */
.card--featured { }
.card__title--large { }

BEM’s double underscore and double hyphen syntax looks unusual at first, but it solves a critical problem: you can look at any class name and immediately understand what it relates to and where it fits in the component hierarchy.

The key discipline with BEM is avoiding nesting. A BEM class should always be a single class selector, never nested inside another. This keeps specificity flat and predictable across your entire stylesheet.

/* Good: flat specificity */
.card__title { font-size: 1.25rem; }

/* Bad: increased specificity, harder to override */
.card .card__title { font-size: 1.25rem; }

File Organisation: ITCSS

Inverted Triangle CSS (ITCSS) is a file organisation methodology created by Harry Roberts ↗. It structures your CSS from the broadest, most generic styles at the top to the most specific, localised styles at the bottom.

The layers, from top to bottom, are:

LayerPurposeOutputs CSS?Specificity
SettingsVariables, design tokens, configurationNoNone
ToolsMixins and functionsNoNone
GenericResets, normalisation, box-sizingYesVery low
ElementsBare HTML element styles (h1, p, a)YesLow
ObjectsLayout primitives (containers, grids)YesLow
ComponentsSpecific UI componentsYesMedium
UtilitiesHelper classes, overridesYesHigh

ITCSS works because it mirrors the natural specificity curve. Generic styles have low specificity and sit at the top. Utilities have high specificity and sit at the bottom. This means you rarely need to fight the cascade.

styles/
  settings/
    _colours.scss
    _typography.scss
    _spacing.scss
  tools/
    _mixins.scss
    _functions.scss
  generic/
    _reset.scss
    _box-sizing.scss
  elements/
    _headings.scss
    _links.scss
  objects/
    _container.scss
    _grid.scss
  components/
    _card.scss
    _header.scss
    _navigation.scss
  utilities/
    _visually-hidden.scss
    _text-align.scss

Utility-First CSS

Tailwind CSS popularised the utility-first approach, where you compose styles directly in your HTML using single-purpose utility classes.

<div class="flex items-center gap-4 rounded-lg bg-white p-6 shadow-md">
  <h2 class="text-lg font-semibold text-grey-900">Card Title</h2>
  <p class="text-sm text-grey-600">Card description text.</p>
</div>

The advantages at scale are significant. Your CSS file size plateaus because utilities are reused rather than duplicated. Naming debates disappear. Deleting a component means deleting its HTML; there are no orphaned style rules to clean up.

The trade-off is that your HTML becomes more verbose, and design consistency depends on your Tailwind configuration (spacing scale, colour palette, typography) being well-defined. Without a strong configuration, utility-first CSS can lead to inconsistent designs just as easily as poorly organised custom CSS.

CriteriaBEM/ITCSSUtility-First (Tailwind)CSS-in-JSCSS Modules
ScopingConvention-basedAtomic classesAutomaticBuild-time
Bundle growthLinear with featuresPlateausLinearLinear
Learning curveModerateLow to moderateModerateLow
Runtime costNoneNonePossible (styled-components)None
Refactoring safetyModerateHighHighHigh
Framework agnosticYesYesNo (React/Vue)Yes
CSS Bundle Size Growth Over 18 Months 0 KB 100 KB 200 KB 300 KB 400 KB 3 months 6 months 12 months 18 months Unstructured BEM/ITCSS Utility-first

Component Scoping

Modern frameworks offer built-in style scoping that solves many of CSS’s global-scope problems.

CSS Modules generate unique class names at build time, ensuring that .title in one component never conflicts with .title in another. They work with any bundler and require no runtime overhead.

Vue’s scoped styles and Svelte’s component styles achieve the same isolation using attribute selectors added at compile time.

Shadow DOM provides the strongest encapsulation through the browser’s built-in scoping mechanism, though styling across the shadow boundary can be challenging.

Whichever approach you use, the principle is the same: styles should be owned by the component they belong to. When you delete a component, its styles should disappear with it. This aligns closely with the broader principle of choosing the right framework for your project’s needs.

Design Tokens

Design tokens are the foundation of a scalable design system. They are named values that represent your design decisions: colours, spacing, typography, shadows, and breakpoints.

:root {
  --colour-primary: #2563eb;
  --colour-text: #1f2937;
  --spacing-sm: 0.5rem;
  --spacing-md: 1rem;
  --spacing-lg: 1.5rem;
  --font-size-body: 1rem;
  --font-size-heading: 1.5rem;
  --radius-md: 0.5rem;
}

Using tokens instead of raw values ensures consistency and makes sweeping design changes trivial. Updating your primary colour means changing one variable, not searching through hundreds of files. The MDN documentation on CSS custom properties ↗ is an excellent starting point for understanding how native CSS variables work.

Auditing and Maintenance

CSS architecture is not a one-time setup. It requires ongoing maintenance to remain effective.

PurgeCSS or Tailwind’s built-in tree-shaking removes unused styles from your production bundle. Run it as part of your build pipeline.

Stylelint enforces your naming conventions, property ordering, and architecture rules automatically. Configure it to reject patterns that violate your chosen methodology.

CSS Stats and Wallace are audit tools that analyse your stylesheet and report on file size, specificity distribution, colour usage, and redundant declarations. Run them periodically to catch drift.

In my experience, scheduling a quarterly CSS audit catches specificity creep and dead code before they become serious problems. On one team, we found that 40% of our CSS declarations were unused after a year of rapid feature development. Combining these audits with automated code quality tools creates a strong feedback loop. A well-maintained CSS architecture also has a direct impact on web performance, since CSS is render-blocking by default.

Choosing Your Approach

There is no single correct CSS architecture. The best choice depends on your team, your framework, and your project’s needs.

For new projects with a component framework (React, Vue, Svelte), CSS Modules or the framework’s built-in scoping combined with design tokens is a strong default. Add Tailwind if your team prefers utility-first development.

For large legacy projects that need incremental improvement, introduce ITCSS to organise existing styles and BEM for new components. Gradually migrate old styles into the new structure.

For design-system-heavy projects where pixel-perfect consistency matters, design tokens with component-scoped custom CSS gives you the most control.

Whatever you choose, document it, enforce it with tooling, and treat it as a team agreement rather than a personal preference. CSS architecture only works when everyone follows it. For practical tips on getting that level of team alignment, have a look at writing documentation developers actually read. If you are also working on accessibility fundamentals, a well-structured CSS architecture makes accessible styling considerably easier to maintain.

Frequently asked questions

Is BEM still relevant in 2026?

Yes, BEM remains one of the most widely used CSS naming conventions, particularly for projects that use traditional stylesheets or CSS modules. Its explicit naming makes stylesheets predictable and easy to navigate. While utility-first frameworks like Tailwind have gained popularity, BEM is still an excellent choice for teams that prefer semantic class names and component-scoped styles.

Should I use Tailwind CSS or write custom CSS for a large project?

Both approaches work at scale, but they require different disciplines. Tailwind excels when your team is large and you want to eliminate naming debates and reduce CSS file growth. Custom CSS (with a methodology like BEM or ITCSS) gives you more control and can produce smaller bundles for design-heavy sites. Many teams use a hybrid approach: Tailwind for layout and spacing utilities, custom CSS for complex component styles.

How do I prevent CSS from growing out of control?

Enforce a clear architecture from day one. Use a methodology (BEM, ITCSS, or utility-first) and document it. Scope styles to components so that changes are local rather than global. Audit your CSS regularly using tools like PurgeCSS to remove unused styles. Most importantly, resist the temptation to add one-off overrides; instead, extend your design system.

What is CSS specificity and why does it cause problems at scale?

Specificity determines which CSS rule wins when multiple rules target the same element. IDs have higher specificity than classes, which have higher specificity than element selectors. In large projects, developers often increase specificity to override existing styles, creating a specificity arms race where rules become increasingly difficult to override or remove. Keeping specificity low and consistent (e.g., using only classes) prevents this.

Should I use CSS-in-JS for a large project?

CSS-in-JS solutions like styled-components or Emotion provide strong component scoping and work well with React-based architectures. They add runtime overhead, which is a consideration for performance-critical applications. Newer zero-runtime alternatives like Vanilla Extract and Panda CSS compile to static CSS at build time, giving you the developer experience of CSS-in-JS without the performance cost.

Enjoyed this article? Get more developer tips straight to your inbox.

Comments

Join the conversation. Share your experience or ask a question below.

0/1000

No comments yet. Be the first to share your thoughts.