Elementary Standards
HTMLCSSWeb StandardsSEOBlogStart reading
Home / Blog / CSS Custom Properties and Design Systems: A Practical Guide
CSS

CSS Custom Properties and Design Systems: A Practical Guide

How to use native CSS custom properties — not Sass variables — as the backbone of a design token system that scales across themes and components.

7 min read·2026-10-12

CSS custom properties (--name: value, read with var()) are not just "CSS variables" — unlike a Sass variable, which is erased at compile time, a custom property lives in the DOM, cascades, inherits, and can be updated at runtime without a rebuild. That single difference is why they're the right foundation for a design token system, and this guide covers the two-layer token model, naming, scoping, theming, and the fallback behavior that trips people up. Teams doing standards-compliant web development at YuSMP treat a token system as a contract, not a decoration — the same discipline applies here.

What custom properties actually are (and why they're not just "CSS variables")

A Sass or LESS variable is a find-and-replace trick: the preprocessor swaps $primary-color for its value and the variable itself disappears from the shipped CSS. A custom property is different in kind. Declare one and it becomes a real, inheritable value in the cascade:

:root {
  --color-blue-600: #0098f2;
}
.button {
  background: var(--color-blue-600);
}

Because it's part of the cascade, a custom property can be redefined at any level of the DOM — a media query, a data attribute, a parent element — and every descendant that reads it with var() updates immediately, with no JavaScript and no rebuild step. That's the capability a compile-time variable simply cannot offer, and it's the reason design systems are migrating their token layer off Sass and onto native custom properties.

--color-blue-600--color-interactivevar(--button-bg).button { }primitivesemanticspacerspacer

The two-layer token model: primitive and semantic

Mature design systems split tokens into two layers instead of one flat list. Primitive tokens hold raw values with no opinion about where they're used — --color-blue-600: #0098f2. Semantic tokens point at a primitive but name a role — --color-interactive: var(--color-blue-600). Components only ever read semantic tokens:

:root {
  /* primitive */
  --color-blue-600: #0098f2;
  --color-gray-900: #0d111b;

  /* semantic */
  --color-interactive: var(--color-blue-600);
  --color-text: var(--color-gray-900);
}
.link { color: var(--color-interactive); }

The indirection is the point. When the brand color changes, you edit one primitive and every semantic token built on it updates. When a component needs a different role — a link that should read as "interactive" but render differently on a dark card — you redefine the semantic token at that scope, and no component CSS has to change at all.

Naming conventions that survive a growing design system

A token system fails long before it runs out of colors; it fails when nobody can guess a token's name. The pattern that holds up is hierarchical and boring on purpose: --<category>-<property>-<variant>, for example --color-bg-subtle, --space-md, --radius-lg. Consistency beats cleverness every time — a predictable, slightly verbose name that a new engineer can guess correctly beats a short one that only the author remembers.

Where should tokens live? Scope and inheritance

Global tokens belong on :root, where every element in the document can read them through inheritance. Component-level tokens belong on the component's own selector, scoped to override just that subtree:

.card {
  --card-padding: 24px;
  padding: var(--card-padding);
}
.card.compact {
  --card-padding: 12px;
}

This is the gap most writing on design tokens skips: a custom property a component exposes on its own root is effectively a public API, the same way a prop or an attribute is. --card-padding is a contract — "you may override this, and only this, from outside" — while everything else inside the component stays encapsulated. Treat the set of custom properties a component declares as its interface boundary, document it the same way you'd document a prop list, and the rest of the token system stays legible as it grows.

Shipping dark mode without a line of JavaScript

Because custom properties cascade, a full theme swap is just redefining the same semantic tokens inside a different scope — no state, no re-render, no JS theme switcher:

:root {
  --color-bg: #ffffff;
  --color-text: #0d111b;
}
[data-theme="dark"] {
  --color-bg: #0d111b;
  --color-text: #ffffff;
}
@media (prefers-color-scheme: dark) {
  :root { --color-bg: #0d111b; --color-text: #ffffff; }
}

Every component that reads var(--color-bg) or var(--color-text) repaints the instant the attribute or media query flips. The theme logic lives entirely in CSS; the only JavaScript you need, if any, is a one-line toggle for the data-theme attribute itself.

Fallback values — the correctness detail everyone skips

var() takes an optional second argument: var(--x, fallback). If --x is unset — not empty, genuinely never declared anywhere in scope — the fallback value is used instead. This matters in two concrete situations: an optional theme token that a smaller or older theme never defines, and third-party embeds or widgets that render inside your page without access to your token sheet. Give those cases an explicit fallback and the component degrades gracefully instead of silently rendering with unset, which for most properties behaves like the property was never applied at all.

.widget {
  background: var(--widget-bg, #f3f2ef);
}

Are custom properties ready for production in 2026?

Yes, without qualification — custom properties and var() have shipped in every evergreen browser for years and need no fallback or feature query. The one genuinely new piece is @property, which lets you register a custom property with a type, an initial value, and whether it inherits — typed custom properties that the browser can now interpolate and animate correctly, something plain untyped custom properties can't do. Support has matured across Chromium and Firefox; treat it as a forward-looking addition for properties you need to animate, not a requirement for the token system itself.

FAQ

What's the difference between CSS custom properties and Sass/SCSS variables?

A Sass variable is replaced at compile time and doesn't exist in the shipped CSS. A CSS custom property is a real, live value in the DOM — it cascades, inherits, and can be redefined at runtime (in a media query, a data attribute, a different scope) with no rebuild.

Should design tokens live in :root or be scoped to components?

Both. Global, system-wide tokens (brand colors, spacing scale, type sizes) belong on :root. Tokens that only one component needs to expose for overriding — like --card-padding — belong scoped to that component's own selector.

Can I use CSS custom properties for a design system without a build tool?

Yes. That's one of their advantages over Sass variables: no compiler is required to define, read, or override them. A build tool (Style Dictionary and similar) is useful once you need to generate tokens for multiple platforms from one source, but it's optional for CSS-only use.

How do custom properties make dark mode easier than a JavaScript theme switcher?

Because the theme values live in the cascade, flipping a data-theme attribute or matching a prefers-color-scheme query redefines the tokens and every component that reads them repaints immediately. There's no component state to manage and no re-render to trigger.

What does var(--x, fallback) actually do, and when do I need it?

It supplies a value to use if --x was never declared in any reachable scope. Use it for optional tokens that not every theme defines, and for components that might render without access to your token sheet, such as third-party widgets.

Are typed custom properties (@property) usable in production yet?

Yes, across current Chromium and Firefox. They're most valuable when you need the browser to interpolate or animate a custom property correctly; for a standard, static token system, plain custom properties remain the default choice.

Do CSS custom properties work with media queries and container queries?

Yes — redefining a custom property inside a media query or a container query is the standard pattern for responsive and theme-aware design systems, and it's exactly how the dark-mode example above works.

Back to the blog
More notes

Keep reading