Back

How to Organize CSS in Modern Web Projects

How to Organize CSS in Modern Web Projects

CSS is easy to write but hard to maintain. A few hundred lines feel manageable until six months later you’re afraid to change anything because you don’t know what will break. The problem isn’t CSS itself — it’s the lack of structure.

Here’s a practical approach to CSS organization that scales, stays readable, and works with modern tooling.

Key Takeaways

  • Use native CSS cascade layers (@layer) to control style order and eliminate specificity battles.
  • Structure design tokens in a primitive-to-semantic hierarchy so retheming requires changes in one place, not dozens.
  • Co-locate component styles with their components using CSS Modules or similar scoping tools.
  • Keep nesting shallow — two levels deep is typically the limit before specificity problems resurface.
  • Adopt a clear file structure that separates global styles from component styles with no unintentional bleed.

Start With a Clear Layer Structure

The most important decision in CSS architecture is controlling where styles live and in what order they apply. Native CSS cascade layers (@layer) make this explicit.

@layer reset, tokens, base, components, utilities;

Declaring layers upfront means later layers win over earlier ones — regardless of specificity. No more fighting the cascade with increasingly specific selectors or !important hacks.

A practical layer order:

  • reset — normalize browser defaults
  • tokens — CSS custom properties (your design tokens)
  • base — element-level styles (body, h1, a)
  • components — scoped UI styles
  • utilities — single-purpose overrides

This structure gives you predictable specificity and a clear mental model for where any given style belongs.

Design Tokens Belong at the Foundation

Design tokens are named values for colors, spacing, typography, and other design decisions. Defined as CSS custom properties, they create a single source of truth across your entire codebase.

:root {
  --color-primary: oklch(55% 0.2 250);
  --space-md: 1rem;
  --font-body: "Inter", sans-serif;
}

Organize tokens from primitive to semantic:

  • Primitive: --blue-500: oklch(55% 0.2 250)
  • Semantic: --color-action: var(--blue-500)
  • Component: --btn-bg: var(--color-action)

This hierarchy means you can retheme an entire project by changing semantic tokens, not hunting through component styles.

Component-Based Styling: Scope Your Styles

Global stylesheets handle base styles. Components handle everything else. The key principle: co-locate styles with the component they belong to.

CSS Modules are the most straightforward way to achieve this in React, Vue, or any bundler-based project. Each .module.css file is locally scoped by default — class names are transformed to unique identifiers at build time, so .button in one component never collides with .button in another.

/* Button.module.css */
.button {
  background: var(--btn-bg);
  padding: var(--space-sm) var(--space-md);
}

Native CSS nesting (now supported in all modern browsers) also reduces the need for preprocessors in component styles:

.card {
  padding: var(--space-md);

  & .card-title {
    font-size: 1.25rem;
  }
}

Keep nesting shallow — two levels is usually enough. Deep nesting recreates the specificity problems you were trying to avoid.

Native @scope is now broadly available, but in application code it’s still less common than CSS Modules or framework-level scoping, so treat it as an emerging option rather than a default.

Utility-First CSS: Where Tailwind v4 Fits

Tailwind CSS v4 takes a different approach: instead of writing component CSS, you compose styles directly in markup using utility classes. Version 4 shifts to a CSS-first configuration model — you configure Tailwind inside a CSS file using @theme, not a JavaScript config.

@import "tailwindcss";

@theme {
  --color-primary: oklch(55% 0.2 250);
}

Tailwind works well for teams that want fast iteration and consistent design constraints. The trade-off is verbose markup and less semantic class names. Many teams use a hybrid approach: Tailwind utilities for layout and spacing, CSS Modules or custom properties for complex component logic.

A Practical File Structure

For most projects, this structure scales well:

styles/
  index.css        ← imports only, declares @layer order
  tokens.css       ← design tokens
  reset.css        ← browser normalization
  base.css         ← element styles
  utilities.css    ← helper classes

components/
  Button/
    Button.jsx
    Button.module.css

Global styles live in styles/. Component styles live next to the component. Nothing bleeds across that boundary without a good reason.

Conclusion

Good CSS organization comes down to a few consistent habits: declare your layer order early, define design tokens at the root, scope component styles locally, and keep selectors shallow. You don’t need a rigid methodology — you need clear conventions your whole team understands and follows.

Start simple. Add structure only when the project demands it.

FAQs

Yes. Tailwind v4 is built around native cascade layers (theme, base, components, utilities). Instead of wrapping its output, you control the cascade by placing your own CSS in the appropriate layer so it composes predictably with Tailwind’s utilities.

They solve different problems. CSS nesting reduces verbosity and groups related rules, but it does not scope class names. CSS Modules guarantee local scope by generating unique identifiers at build time. For projects where multiple components might share class names like button or title, CSS Modules remain the more reliable isolation mechanism.

Every design token is a CSS custom property, but not every custom property is a design token. Design tokens represent deliberate design decisions such as brand colors, spacing scales, and type sizes. They are organized in a primitive-to-semantic hierarchy so that changing a single semantic token can retheme an entire project without editing individual component styles.

Utility-first CSS like Tailwind works best for rapid prototyping and teams that prefer co-locating style decisions in markup. Component-scoped CSS suits projects with complex UI logic or strict separation of concerns. Many teams combine both, using utilities for layout and spacing while reserving scoped stylesheets for stateful or heavily customized components.

Truly understand users experience

See every user interaction, feel every frustration and track all hesitations with OpenReplay — the open-source digital experience platform. It can be self-hosted in minutes, giving you complete control over your customer data. . Check our GitHub repo and join the thousands of developers in our community..

OpenReplay