CSS: Custom Properties (CSS Variables) vs. Sass/Less Variables
A classic frontend architecture and system design question often asked in interviews is:
What are CSS Custom Properties (CSS Variables)? How do they differ from preprocessor variables (like Sass or Less), and what benefits do they bring to dynamic components, layout systems, and dark mode theme structures?
Historically, preprocessors like Sass or Less were essential to introduce variables into stylesheets. Today, native CSS Custom Properties offer powerful runtime capabilities that build-time tools simply cannot replicate.
1. The Real-World Analogy
To understand the core difference between the two, think of them like this:
- Sass/Less Variables are like "Ink on Paper" (Printed Menu): When you print a menu, the prices and items are locked in. If you want to change a price or offer a lunch discount, you have to reprint the entire menu (recompile your Sass to CSS). The customer (browser) only sees the final printed paper; they have no idea what your digital template file looked like.
- CSS Custom Properties are like "Digital LED Screens" (Dynamic Menu): The screen hangs on the wall, and you can change any price or color instantly with a remote control (JavaScript), or it can automatically shift to a "happy hour" theme when the sun goes down (Media Queries / Dark Mode). The browser sees and understands the variables in real time.
2. Technical Comparison
| Feature | CSS Custom Properties (Native) | Preprocessor Variables (Sass/Less) |
|---|---|---|
| Resolution Time | Runtime (in the browser) | Compile Time (during build/pre-processing) |
| DOM Awareness | Yes (follows the cascade and inheritance) | No (lexical block scoping, ignored by DOM) |
| JavaScript Access | Yes (read/write dynamically at runtime) | No (variables are destroyed after compiling) |
| Media Query Support | Yes (values can change inside @media blocks) | No (cannot redefine variable inside media query) |
| Fallback Values | Yes (integrated into the var() syntax) | No (relies on build-time compiler defaults) |
3. How They Work Under the Hood
A. Preprocessor Variables (Compile Time)
Sass variables exist only in your source code. During compilation, the compiler replaces every variable reference with its static value.
Sass Input:
$primary-color: #3b82f6;
.button {
background-color: $primary-color;
}
.alert {
color: $primary-color;
}Compiled CSS Output (What the browser gets):
.button {
background-color: #3b82f6; /* Variable is gone, replaced with static value */
}
.alert {
color: #3b82f6; /* Variable is gone, replaced with static value */
}B. CSS Custom Properties (Runtime & Cascade)
Native variables are kept intact in the final CSS. The browser computes their values on the fly, respecting the DOM tree's cascade and inheritance rules.
/* Define globally on the root element */
:root {
--main-color: #3b82f6;
--padding-size: 16px;
}
/* Inherits the global value */
.card {
padding: var(--padding-size);
border: 2px solid var(--main-color);
}
/* Override locally for this specific container and all of its children */
.error-theme {
--main-color: #ef4444;
}Why this is powerful:
If you add the .error-theme class to a .card element, the browser automatically recalculates --main-color to #ef4444 for that card and any components inside it, without needing to write duplicate CSS selectors.
4. Practical Implementation Examples
Example 1: Streamlined Dark Mode (No Duplicate Selectors)
With Sass, dark mode often requires duplicating selectors or nesting styles extensively. With CSS Custom Properties, you only toggle the values of the variables.
/* 1. Establish the theme variables */
:root {
--bg-color: #ffffff;
--text-color: #1f2937;
--card-bg: #f3f4f6;
}
/* 2. Overwrite values for the dark theme scope */
[data-theme="dark"] {
--bg-color: #111827;
--text-color: #f9fafb;
--card-bg: #1f2937;
}
/* 3. Style components using the variables */
body {
background-color: var(--bg-color);
color: var(--text-color);
transition: background-color 0.3s ease, color 0.3s ease;
}
.card {
background-color: var(--card-bg);
border: 1px solid var(--text-color);
}To switch themes, JavaScript simply updates the data-theme attribute on the root html node:
// Toggle to dark mode
document.documentElement.setAttribute('data-theme', 'dark');Example 2: Interactive JavaScript Control (e.g., Dynamic Custom Cursors / Positioning)
Because CSS variables are dynamic, you can update them using JavaScript to build complex interactive components with high performance.
CSS:
.glow-effect {
/* Default position */
--mouse-x: 50%;
--mouse-y: 50%;
background: radial-gradient(circle at var(--mouse-x) var(--mouse-y), rgba(59, 130, 246, 0.4) 0%, transparent 60%);
}JavaScript:
const element = document.querySelector('.glow-effect');
element.addEventListener('mousemove', (e) => {
const rect = element.getBoundingClientRect();
const x = ((e.clientX - rect.left) / rect.width) * 100;
const y = ((e.clientY - rect.top) / rect.height) * 100;
// Dynamically update the CSS variables on hover
element.style.setProperty('--mouse-x', `${x}%`);
element.style.setProperty('--mouse-y', `${y}%`);
});5. Common Interview Pitfalls & Solutions
❌ Pitfall 1: Trying to use Sass color functions on CSS variables
You cannot pass a dynamic native variable to a compiler-based Sass function like lighten(), darken(), or rgba() because the Sass compiler does not know what the value of the CSS variable is at compile-time.
/* ❌ THIS WILL FAIL TO COMPILE OR RENDER INCORRECTLY */
$color: var(--primary-color);
.button {
background-color: lighten($color, 10%);
}✅ The Modern CSS Solution: color-mix()
Modern CSS supports the native color-mix() function, allowing you to manipulate colors directly in the browser using custom properties.
/* ✅ NATIVE RUNTIME COLOR MIXING */
.button {
background-color: color-mix(in srgb, var(--primary-color) 90%, white); /* Makes it 10% lighter */
}
.button:hover {
background-color: color-mix(in srgb, var(--primary-color) 80%, black); /* Makes it 20% darker */
}❌ Pitfall 2: Forgetting fallback values
If a custom property fails to load or is not defined, the style falls back to the browser default (or inherits nothing), which can break your layout.
/* ❌ Risky if --brand-color is not declared */
.button {
background-color: var(--brand-color);
}✅ The Solution: Nested Fallbacks
Always supply a sensible fallback value. You can even chain them together:
/* ✅ Safe implementation with fallback */
.button {
background-color: var(--brand-color, #3b82f6);
}
/* ✅ Nested fallbacks */
.button-text {
color: var(--brand-text, var(--fallback-text, #ffffff));
}Senior-Level Interview Answer
"The fundamental difference is execution time: Sass variables are compiled away at build time, while CSS Custom Properties are evaluated at runtime by the browser.
Because CSS Custom Properties live in the DOM, they are aware of HTML structure. This allows them to inherit values down the cascade, change dynamically based on media queries (like
@media (prefers-color-scheme: dark)), and interface directly with JavaScript viastyle.setProperty().In modern frontend architecture, they are the standard for themes (like dark mode) and reactive design systems. Instead of writing duplicate selector blocks for different layouts or color schemes, we define clean CSS declarations and toggle theme variables. For color transformations, we pair them with modern native tools like
color-mix(), avoiding preprocessor build dependencies altogether."
Key Takeaways
- Sass/Less Variables = Build-Time: Perfect for internal preprocessor structures, math, configure-once constants, and static library themes.
- CSS Custom Properties = Run-Time: Essential for interactive UI, dynamic configurations, user-customizable color schemes, and system-level themes.
- Inheritance Rules: Custom properties cascade down. Redefining a variable in
.dark-cardonly affects elements inside.dark-card. - JS Interoperability: Retrieve values using
getComputedStyle(el).getPropertyValue('--name')and modify them usingel.style.setProperty('--name', 'value'). - Always Provide Fallbacks: Ensure robust UI design by using the fallback parameter
var(--color, #ccc)to guard against missing variables.
