DEV Community

Cover image for CSS Architecture That Actually Scales: Design Tokens, Custom Properties, and the Systems That Hold Up in Production
Sara Casciaro
Sara Casciaro

Posted on

CSS Architecture That Actually Scales: Design Tokens, Custom Properties, and the Systems That Hold Up in Production

Every frontend developer has inherited a CSS codebase where the color #2D5BE3 appears 47 times, where there are four different definitions of "spacing-medium" and none of them match, and where changing the primary button color requires a find-and-replace across twelve files that will inevitably miss two.

This is not a discipline problem. It is an architecture problem. And the solution is not more comments in the code or better naming conventions on their own. It is building a system that makes inconsistency structurally difficult.

This guide covers the practical implementation of CSS custom properties as a design token layer, naming conventions that survive team growth, and the component architecture patterns that hold up across real production projects.

What Design Tokens Actually Are (And What They Are Not)

Design tokens are the named values that represent the decisions of a design system: colors, spacing, typography scales, border radii, shadow levels. They are not the components themselves. They are the raw material that components are built from.

The distinction matters because design tokens exist at a level of abstraction above the implementation. A token named --color-brand-primary is a design decision. The hex value it holds is an implementation detail. When the brand primary color changes from #2D5BE3 to #1A4CD1, you change one value in one place and every component that references that token updates automatically.

Without tokens, a color change is a search-and-replace exercise with a margin of error. With tokens, it is a single line edit.

The Three Levels of a Token System

A well-structured token system has three levels, each with a distinct purpose.

Primitive tokens are the raw values with no semantic meaning. They define the full range of options available in the system:

:root {
  /* Color primitives */
  --color-blue-100: #E8F0FD;
  --color-blue-200: #C5D8FB;
  --color-blue-300: #8FB5F8;
  --color-blue-400: #5A92F5;
  --color-blue-500: #2D5BE3;
  --color-blue-600: #1A4CD1;
  --color-blue-700: #1240B8;

  /* Spacing primitives */
  --space-1: 0.25rem;   /* 4px */
  --space-2: 0.5rem;    /* 8px */
  --space-3: 0.75rem;   /* 12px */
  --space-4: 1rem;      /* 16px */
  --space-6: 1.5rem;    /* 24px */
  --space-8: 2rem;      /* 32px */
  --space-12: 3rem;     /* 48px */
  --space-16: 4rem;     /* 64px */
}
Enter fullscreen mode Exit fullscreen mode

Semantic tokens map primitive values to their intended use. This is where meaning enters the system:

:root {
  /* Semantic color tokens */
  --color-action-primary: var(--color-blue-500);
  --color-action-primary-hover: var(--color-blue-600);
  --color-action-primary-active: var(--color-blue-700);
  --color-surface-default: #FFFFFF;
  --color-surface-subtle: var(--color-blue-100);
  --color-text-primary: #1A1A2E;
  --color-text-secondary: #4A4A6A;
  --color-text-on-action: #FFFFFF;
  --color-border-default: #E2E4E9;
  --color-border-focus: var(--color-blue-500);

  /* Semantic spacing tokens */
  --spacing-component-padding-sm: var(--space-2) var(--space-3);
  --spacing-component-padding-md: var(--space-3) var(--space-4);
  --spacing-component-padding-lg: var(--space-4) var(--space-6);
  --spacing-section-gap: var(--space-12);
  --spacing-content-gap: var(--space-6);
}
Enter fullscreen mode Exit fullscreen mode

Component tokens are the most specific layer, scoped to individual components:

.button {
  --button-bg: var(--color-action-primary);
  --button-bg-hover: var(--color-action-primary-hover);
  --button-color: var(--color-text-on-action);
  --button-padding: var(--spacing-component-padding-md);
  --button-radius: var(--radius-md);

  background-color: var(--button-bg);
  color: var(--button-color);
  padding: var(--button-padding);
  border-radius: var(--button-radius);
  transition: background-color 150ms ease;
}

.button:hover {
  background-color: var(--button-bg-hover);
}
Enter fullscreen mode Exit fullscreen mode

The three-layer structure means that when the design team decides to change the brand primary from blue to green, you update two primitive values and every semantic and component token that references them updates automatically across the entire codebase.

Naming Conventions That Survive Handoffs

Token naming is where most systems break down in practice. The name needs to communicate enough information to be usable without documentation, survive being read in isolation, and remain valid when the underlying value changes.

The pattern that holds up best across team sizes is category-property-variant-state:

--[category]-[property]-[variant?]-[state?]
Enter fullscreen mode Exit fullscreen mode

Applied to colors:

/* category: color */
/* property: text, surface, border, action */
/* variant: primary, secondary, danger, success */
/* state: default (omitted), hover, active, disabled */

--color-text-primary
--color-text-secondary
--color-text-disabled
--color-surface-default
--color-surface-subtle
--color-surface-overlay
--color-action-primary
--color-action-primary-hover
--color-action-danger
--color-action-danger-hover
--color-border-default
--color-border-focus
--color-border-error
Enter fullscreen mode Exit fullscreen mode

Applied to spacing:

/* category: spacing */
/* property: component, section, layout */
/* variant: padding, gap, margin */
/* variant modifiers: sm, md, lg, xl */

--spacing-component-padding-sm
--spacing-component-padding-md
--spacing-component-padding-lg
--spacing-section-gap
--spacing-layout-sidebar-width
Enter fullscreen mode Exit fullscreen mode

The rule worth enforcing strictly: never use a primitive token directly in a component. Components reference semantic tokens. Semantic tokens reference primitives. This indirection is what allows global changes without manual search-and-replace.

Responsive Typography Without Media Query Repetition

Typography is the area where CSS architecture becomes most visibly inconsistent across large codebases. The typical outcome is type styles defined in twelve different places, with slight variations that nobody can explain.

A type scale defined as custom properties solves this:

:root {
  /* Type scale primitives */
  --font-size-xs: 0.75rem;
  --font-size-sm: 0.875rem;
  --font-size-base: 1rem;
  --font-size-lg: 1.125rem;
  --font-size-xl: 1.25rem;
  --font-size-2xl: 1.5rem;
  --font-size-3xl: 1.875rem;
  --font-size-4xl: 2.25rem;

  --line-height-tight: 1.25;
  --line-height-snug: 1.375;
  --line-height-normal: 1.5;
  --line-height-relaxed: 1.625;

  --font-weight-regular: 400;
  --font-weight-medium: 500;
  --font-weight-semibold: 600;
  --font-weight-bold: 700;

  /* Semantic type tokens */
  --text-heading-1: var(--font-size-4xl);
  --text-heading-2: var(--font-size-3xl);
  --text-heading-3: var(--font-size-2xl);
  --text-body: var(--font-size-base);
  --text-small: var(--font-size-sm);
  --text-caption: var(--font-size-xs);
}
Enter fullscreen mode Exit fullscreen mode

For responsive type without repetitive media queries, CSS clamp() applied at the semantic token level:

:root {
  --text-heading-1: clamp(1.875rem, 4vw + 0.5rem, 3rem);
  --text-heading-2: clamp(1.5rem, 3vw + 0.5rem, 2.25rem);
  --text-heading-3: clamp(1.25rem, 2vw + 0.5rem, 1.875rem);
}
Enter fullscreen mode Exit fullscreen mode

The heading size now scales fluidly between viewport widths without a single media query. Every component that uses var(--text-heading-1) gets responsive type automatically.

Dark Mode Without Duplication

Dark mode implemented as a separate stylesheet that overrides everything is a maintenance problem. Every time you add a new color to the light mode, you need to remember to add its dark mode equivalent. You will forget. Everybody forgets.

The correct implementation uses semantic tokens that get redefined under a dark mode selector:

/* Light mode defaults (on :root) */
:root {
  --color-surface-default: #FFFFFF;
  --color-surface-subtle: #F5F7FA;
  --color-text-primary: #1A1A2E;
  --color-text-secondary: #4A4A6A;
  --color-border-default: #E2E4E9;
}

/* Dark mode overrides */
@media (prefers-color-scheme: dark) {
  :root {
    --color-surface-default: #1A1A2E;
    --color-surface-subtle: #242438;
    --color-text-primary: #F0F0F8;
    --color-text-secondary: #A0A0C0;
    --color-border-default: #3A3A5A;
  }
}

/* Or via class for user toggle */
[data-theme="dark"] {
  --color-surface-default: #1A1A2E;
  --color-surface-subtle: #242438;
  --color-text-primary: #F0F0F8;
  --color-text-secondary: #A0A0C0;
  --color-border-default: #3A3A5A;
}
Enter fullscreen mode Exit fullscreen mode

Every component that uses semantic tokens gets dark mode support automatically. No duplication. No forgetting. The component layer never needs to know about the current color scheme.

Component Scoping That Prevents Leakage

One of the persistent problems in CSS architecture is style leakage: styles defined for one component that accidentally affect another. Custom properties help here because they are inherited but not globally scoped in the same way class-based styles are.

The pattern for component isolation:

/* Define component-level overrides at the component root */
.card {
  --card-bg: var(--color-surface-default);
  --card-border: var(--color-border-default);
  --card-padding: var(--spacing-component-padding-lg);
  --card-radius: var(--radius-lg);

  background: var(--card-bg);
  border: 1px solid var(--card-border);
  padding: var(--card-padding);
  border-radius: var(--card-radius);
}

/* Variants override only the component tokens, not global tokens */
.card--highlighted {
  --card-bg: var(--color-surface-subtle);
  --card-border: var(--color-action-primary);
}

/* Children reference the component token, not the global one */
.card__header {
  padding-bottom: var(--spacing-component-padding-md);
  border-bottom: 1px solid var(--card-border);
}
Enter fullscreen mode Exit fullscreen mode

This pattern means the .card__header component border color is controllable at the .card level without touching the global border token. A .card--highlighted changes the header border automatically because both reference --card-border.

The Tooling Question

Design tokens defined in CSS custom properties work without any build tool. They are native CSS that any browser supports. This is their main practical advantage over token systems that require a preprocessor or a JavaScript runtime.

If your project already uses a build pipeline and wants to maintain a single source of truth for tokens across CSS, JavaScript, and potentially native mobile, tools like Style Dictionary or Theo can export tokens from a JSON source to multiple output formats simultaneously.

But for most projects, starting with CSS custom properties directly, without additional tooling, is the right call. The system works, it's debuggable in DevTools, it doesn't introduce build-step dependencies, and it can be extended with tooling later if the project grows to need it.

What You Get at the End

A CSS codebase built on this architecture produces four concrete outcomes that you will notice in daily development.

Global changes are single-line edits. Changing the primary color, the base spacing unit, or the heading type scale requires editing one value in one place.

Component variants are additive. A new button variant adds two or three token overrides and inherits everything else. It does not copy a block of CSS and modify it.

Dark mode is automatic. Every component built on semantic tokens responds to the color scheme change without any component-level code.

Handoffs are survivable. A developer who joins the project six months later can read a component's custom property definitions and understand exactly what values it uses and why, without reading the full stylesheet.

The upfront investment in this architecture pays back on the first significant change request. Which, on any real project, arrives before the first sprint is done.


Written by Sara Casciaro, founder of Sabriel Agency, digital studio in Ugento (LE), Italy.

Top comments (0)