Structuring CSS Variables for Design Systems
CSS Custom Properties (variables) are not just search-and-replace tools like SASS variables. They are live, cascading properties that exist in the DOM. Structuring them correctly is the foundation of a robust design system.
1. The Two-Tier System
Never use raw color values directly on semantic tokens. Always use a two-tier approach: Global Tokens and Semantic Tokens.
:root {
/* Tier 1: Global/Primitive Tokens (The Palette) */
--blue-100: #e6f0ff;
--blue-500: #0055ff;
--blue-900: #001b4d;
--gray-100: #f4f4f4;
--gray-900: #111111;
/* Tier 2: Semantic Tokens (The Usage) */
--color-bg-primary: var(--gray-100);
--color-text-primary: var(--gray-900);
--color-action: var(--blue-500);
--color-action-hover: var(--blue-900);
}
Why? If you want to change the primary action color to red for a specific theme or component, you only change the mapping of --color-action, leaving --blue-500 untouched for places that actually need blue regardless of theme.
2. Handling Opacity with RGB Components
A common pitfall with hex variables is that you cannot easily apply an alpha channel to them in CSS (without using newer color-mix functions). The traditional, most compatible approach is to store the RGB comma-separated components.
:root {
/* Store components, not the full rgb() function */
--rgb-brand: 255, 51, 31;
}
.button {
/* Use full opacity */
background-color: rgb(var(--rgb-brand));
}
.button-faded {
/* Apply opacity easily */
background-color: rgba(var(--rgb-brand), 0.1);
}
3. Scoping Variables
Because CSS variables cascade, you can scope them to specific components. This allows you to create generic component structures that are themed purely by updating locally scoped variables.
.card {
--card-bg: var(--color-bg-primary);
--card-pad: 2rem;
background: var(--card-bg);
padding: var(--card-pad);
}
.card--dark {
/* Override locally without writing new background properties */
--card-bg: var(--gray-900);
color: white;
}