DEV Community

Cover image for **Modern CSS Architecture: From Spaghetti Code to Scalable Design Systems in 2024**
Nithin Bharadwaj
Nithin Bharadwaj

Posted on

**Modern CSS Architecture: From Spaghetti Code to Scalable Design Systems in 2024**

As a best-selling author, I invite you to explore my books on Amazon. Don't forget to follow me on Medium and show your support. Thank you! Your support means the world!

I remember when building websites felt like trying to organize a giant, tangled ball of yarn. You’d have one massive CSS file—often thousands of lines long—and changing a single button color could break something completely unrelated on the other side of the site. We called it the “CSS spaghetti” problem. Styles were global, cascading in unpredictable ways, and our only tool for managing it was trying to write even more specific selectors, which only made things worse.

That old way doesn’t work for the complex, interactive applications we build today. The evolution of CSS architecture has been a search for sanity, for systems that are predictable, easy to maintain, and performant. Let’s walk through how we got here.

It started with a simple but powerful idea: what if styles were tied directly to the components they were meant for, instead of floating in a global pool? This thinking gave rise to methodologies like BEM (Block, Element, Modifier). It was a naming convention, a discipline, to make our intentions clear in the class names themselves.

/* BEM syntax example */
.card { } /* The Block */
.card__header { } /* An Element of the block */
.card--featured { } /* A Modifier that changes the block's state */
Enter fullscreen mode Exit fullscreen mode

This was a huge step forward. Just by looking at the HTML, you could understand the relationship. .card__header clearly belongs to a card. It prevented a lot of accidental clashes. But it was still just a convention. We had to manually enforce it, and the compiled CSS was still one big file. It solved the naming problem but not the architectural one.

Then came a more radical solution: CSS Modules. This was a shift from convention to tooling. The idea is that when you import a CSS file into a JavaScript module, the class names inside it are automatically transformed to be unique. You write normal CSS, but the output is scoped only to your component.

Here’s how it looks in practice. You create a CSS file for your button component.

/* Button.module.css */
.primary {
    background-color: #2563eb;
    color: white;
    padding: 0.75rem 1.5rem;
    border: none;
    border-radius: 0.375rem;
    font-weight: 600;
}

.primary:hover {
    background-color: #1d4ed8;
}
Enter fullscreen mode Exit fullscreen mode

Then, in your component, you import it as if it were a JavaScript object. The class .primary becomes a unique string like Button_primary__abc123.

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

function Button({ children }) {
    return (
        <button className={styles.primary}>
            {children}
        </button>
    );
}
Enter fullscreen mode Exit fullscreen mode

This was a revelation for me. I could finally write simple, flat CSS without worrying about crafting a perfect, unique selector name. The build tool handled the scoping. It felt safe. The component owned its styles, and those styles couldn’t leak out or be overridden from outside. It brought a level of isolation that component-based frameworks like React were promising for logic, but for styling.

Around the same time, another approach was gaining momentum: CSS-in-JS. The most famous library is Styled-components. The core concept is to define your styles in your JavaScript file using template literals. The styles are dynamically applied, and the library ensures they are scoped.

import styled from 'styled-components';

const StyledButton = styled.button`
    background-color: #2563eb;
    color: white;
    padding: 0.75rem 1.5rem;
    border-radius: 0.375rem;
    font-weight: 600;
    border: none;

    &:hover {
        background-color: #1d4ed8;
    }
`;

function Button({ children }) {
    return <StyledButton>{children}</StyledButton>;
}
Enter fullscreen mode Exit fullscreen mode

The developer experience is fantastic. You have the full power of JavaScript to conditionally change styles based on props, and everything co-locates with your component. I’ve built many apps this way. However, there’s a cost. This method typically runs in the browser’s JavaScript runtime. The library must inject <style> tags into the document head as components render. For very large applications, this can impact performance and hinder caching.

This led to the next evolution: zero-runtime CSS-in-JS. Tools like Vanilla Extract work by letting you write your styles in JavaScript or TypeScript, but they extract and compile them into static .css files at build time. You get the type safety and developer ergonomics of writing styles in JS, but the performance and cacheability of plain CSS.

// buttonStyles.css.ts - With Vanilla Extract
import { style } from '@vanilla-extract/css';

export const primary = style({
    backgroundColor: '#2563eb',
    color: 'white',
    padding: '0.75rem 1.5rem',
    borderRadius: '0.375rem',
    fontWeight: 600,
    border: 'none',
    selectors: {
        '&:hover': {
            backgroundColor: '#1d4ed8'
        }
    }
});

// Button.tsx
import * as styles from './buttonStyles.css';

function Button({ children }) {
    return <button className={styles.primary}>{children}</button>;
}
Enter fullscreen mode Exit fullscreen mode

The buttonStyles.css.ts file doesn’t ship to the browser. Instead, during your build process, it generates a static CSS class, much like CSS Modules does. This approach feels like the best of both worlds for many projects.

Parallel to these scoping solutions, a fundamentally different philosophy emerged: utility-first CSS. Tailwind CSS is the flagbearer here. Instead of writing semantic CSS classes like .btn-primary, you apply many small, single-purpose utility classes directly in your HTML.

The provided example shows the shift perfectly. Where before you’d author a .card class, with utility-first you compose the look directly.

<!-- The new way, with utilities -->
<div class="bg-white rounded-xl p-6 shadow-md">
  <h3 class="text-xl font-semibold mb-4">Product Details</h3>
</div>
Enter fullscreen mode Exit fullscreen mode

When I first saw this, I hated it. My HTML looked messy and verbose. It felt like going back to inline styles. But then I tried it on a real project. The consistency was incredible. By restricting choices to a predefined design system (like specific shades of blue, specific spacing scales), the entire UI became more uniform by default. I stopped naming things. I stopped context-switching between HTML and CSS files. The performance was excellent because you only ship the utility classes you actually use.

It’s a different mental model. You’re not designing in a stylesheet; you’re assembling a design directly in your templates with a constrained set of building blocks. It’s not for every project or every team, but its popularity speaks to how well it solves the maintainability problem for many.

While these tools and methodologies were developing, the CSS language itself was not standing still. New native features have begun to solve problems we previously needed frameworks for. Let’s talk about a few that are changing the game.

First, CSS Nesting. For years, we’ve used preprocessors like Sass for this. Now, it’s native in browsers. It allows you to write styles in a hierarchy that mirrors your HTML structure, which is much more readable and maintainable.

.card {
    background: white;
    border-radius: 0.5rem;
    padding: 1.5rem;

    /* Nested selector for .card .header */
    .header {
        margin-bottom: 1rem;

        /* Nested selector for .card .header .title */
        .title {
            font-size: 1.25rem;
            font-weight: 600;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

This is a quality-of-life improvement that makes authoring component-scoped CSS, whether in a module or a native layer, much cleaner.

Second, and perhaps more revolutionary, are Container Queries. For over a decade, we’ve designed responsive layouts based on the viewport size using media queries. But a component on a wide sidebar should probably look different than the same component in a narrow main column, even if the viewport is the same. Container queries let a component adapt to the size of its parent container.

Here’s how it works. You first declare an element as a container.

.component {
    container-type: inline-size; /* Tracks the inline (usually width) dimension */
    width: 100%;
}
Enter fullscreen mode Exit fullscreen mode

Then, you can write styles that apply when that container is a certain size.

@container (min-width: 400px) {
    .component {
        display: flex;
        gap: 1rem;
    }
}

@container (min-width: 600px) {
    .component .sidebar {
        width: 300px;
    }
}
Enter fullscreen mode Exit fullscreen mode

This is a fundamental shift. It enables truly context-aware, reusable components. A card component can have a compact layout inside a sidebar and an expanded layout in the main content area, all based on the space it’s actually given, not the browser window. It makes component libraries far more flexible.

Third, Cascade Layers (@layer). This feature finally gives us declarative control over the CSS cascade. The problem has always been specificity wars. A utility class might be overridden by a component class because of source order, forcing us to use !important or awkward selectors. Layers let us define a clear hierarchy.

You define your layers up front, in the order you want them to cascade.

@layer reset, base, components, utilities;
Enter fullscreen mode Exit fullscreen mode

Now, you can put your styles into these layers. Styles in a later layer (like utilities) will win over styles in an earlier layer (like components), regardless of selector specificity or source order.

@layer components {
    .btn { background-color: gray; }
}

@layer utilities {
    .bg-blue { background-color: blue; }
}

/* In your HTML: <button class="btn bg-blue"> */
/* The button will be blue, because the 'utilities' layer wins. */
Enter fullscreen mode Exit fullscreen mode

This is a powerful tool for creating maintainable, predictable style systems. You can have a low-specificity reset layer, a base layer for element styles, a components layer, and a high-priority utilities layer, all without conflict.

Underpinning many of these modern approaches is the use of CSS Custom Properties, or CSS variables. They are the key to dynamic design systems and theming.

You can define your design tokens—colors, spacing, fonts—as variables on the :root element.

:root {
    --color-primary: #3b82f6;
    --color-surface: #ffffff;
    --spacing-md: 1rem;
    --radius-md: 0.375rem;
}
Enter fullscreen mode Exit fullscreen mode

Then, use them throughout your styles.

.card {
    background: var(--color-surface);
    padding: var(--spacing-md);
    border-radius: var(--radius-md);
}

.btn {
    background-color: var(--color-primary);
}
Enter fullscreen mode Exit fullscreen mode

The magic happens when you want a dark theme. You don’t rewrite all your CSS. You just re-define those variables in a different context.

[data-theme="dark"] {
    --color-primary: #60a5fa;
    --color-surface: #1f2937;
}
Enter fullscreen mode Exit fullscreen mode

Switching themes becomes trivial for JavaScript—just toggle an attribute on the <html> element. The browser repaints everything automatically. It’s performant and elegant. I use this pattern in every project now. It creates a single source of truth for your design values.

Finally, modern CSS is also giving us tools to write more performant styles from the start. Properties like content-visibility can dramatically improve rendering performance for long pages.

.long-list {
    content-visibility: auto;
}
Enter fullscreen mode Exit fullscreen mode

This tells the browser it can skip rendering elements that are off-screen. As the user scrolls, they become visible. It’s like lazy-loading for rendering. You must use it with contain-intrinsic-size to reserve space and avoid layout shifts, but the performance gains can be massive.

We also have better control over user preferences. The prefers-reduced-motion and prefers-color-scheme media queries allow us to build accessible, user-centric experiences by default.

.animated-element {
    transition: transform 0.3s ease;
}

@media (prefers-reduced-motion: reduce) {
    .animated-element {
        transition: none;
    }
}
Enter fullscreen mode Exit fullscreen mode

So, where does this leave us? The evolution isn't about one tool winning. It’s about a set of principles that have emerged: scoping, composition, systematic design, and leveraging the platform.

You might choose Tailwind for its speed and constraint-based workflow on a marketing site. You might choose CSS Modules or Vanilla Extract for a large, complex web app where you want strong isolation and type safety. You might use native features like Container Queries and Layers to build a resilient component library.

The goal remains the same as it was when we were untangling that ball of spaghetti: to write styles that are easy to change, that don’t break unexpectedly, and that help us build better user experiences faster. The difference now is that we have a robust, mature set of tools and native browser features to help us get there. We’re not just writing CSS; we’re designing systems. And that’s a much more interesting problem to solve.

📘 Checkout my latest ebook for free on my channel!

Be sure to like, share, comment, and subscribe to the channel!


101 Books

101 Books is an AI-driven publishing company co-founded by author Aarav Joshi. By leveraging advanced AI technology, we keep our publishing costs incredibly low—some books are priced as low as $4—making quality knowledge accessible to everyone.

Check out our book Golang Clean Code available on Amazon.

Stay tuned for updates and exciting news. When shopping for books, search for Aarav Joshi to find more of our titles. Use the provided link to enjoy special discounts!

Our Creations

Be sure to check out our creations:

Investor Central | Investor Central Spanish | Investor Central German | Smart Living | Epochs & Echoes | Puzzling Mysteries | Hindutva | Elite Dev | Java Elite Dev | Golang Elite Dev | Python Elite Dev | JS Elite Dev | JS Schools


We are on Medium

Tech Koala Insights | Epochs & Echoes World | Investor Central Medium | Puzzling Mysteries Medium | Science & Epochs Medium | Modern Hindutva

Top comments (0)