By VONA
CSS Variables as the Foundation of a Design System
How to build a robust, maintainable design system with native CSS custom properties — without build tools and without overhead.
CSS custom properties — often referred to as CSS variables — have been available in all modern browsers for years. Yet in many projects they’re still used half-heartedly: a few colors at the top of the stylesheet, hard-coded values everywhere else. That wastes considerable potential. If you consistently use CSS variables as the foundation, you get a design system that is maintainable, consistent and adaptive — and all that without a build tool, Sass or an external library.
The decisive difference between Sass variables and native CSS custom properties lies in the runtime. Sass variables are resolved at build time — the resulting CSS contains only the final values. CSS custom properties exist in the browser and can be changed dynamically: via JavaScript, via media query, via a :root override in a specific context. That’s the reason dark mode works so elegantly with CSS variables.
Structure and Naming
A good structure for CSS variables separates primitive values from semantic tokens. Primitive values are the raw design decisions: --color-gray-900: #111111. Semantic tokens reference primitives and describe their function: --color-text: var(--color-gray-900). This two-layer structure makes it possible to override only the semantic tokens when switching themes, without touching the primitives. Dark mode then simply means: [data-theme="light"] { --color-bg: var(--color-gray-100); --color-text: var(--color-gray-900); } — the rest adapts automatically.
For spacing, font sizes and other scales, systematic naming is the way to go: --space-xs, --space-sm, --space-md, --space-lg, --space-xl and so on. If you define this scale carefully at the beginning of a project and use it consistently, you prevent the typical problem of inconsistent spacing, where some elements have 16px of spacing, others 15px and still others 17px — because every developer set their own value.
Theming and Context Variables
Another use case that is often overlooked: context-dependent variables. Within a component, you can define local CSS variables that apply only in that context. A card component can define --card-bg as a variable and override it locally — without needing global classes or inline styles. That makes components flexible and reusable, without hard-coding their behavior too rigidly from the outside.
- Primitive tokens for raw values, semantic tokens for functions
- Consistent naming with a prefix by category (color, space, font)
- Control dark mode by overriding semantic tokens
- Local variables in components for flexible reuse
- Use CSS variables via JavaScript for dynamic adjustments
CSS custom properties are no substitute for a well-thought-out design system — but they are the best native foundation CSS offers for building one. If you use them consistently, you’ll have fewer surprises when the design changes, fewer inconsistent values in the code and a significantly better basis for future extensions.