New: We've launched a brand new Coding Challenges section! Check out these interactive, real-world exercises to level up your skills.Explore Challenges
FrontendPrep
cssEasy

CSS: Custom Properties (CSS Variables) vs. Sass/Less Variables

Loading...

Understand runtime CSS Custom Properties, scope cascading, JS integration for dynamic themes, and why they outshine preprocessor compilation variables.

Arvind M
Arvind MLinkedIn

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

FeatureCSS Custom Properties (Native)Preprocessor Variables (Sass/Less)
Resolution TimeRuntime (in the browser)Compile Time (during build/pre-processing)
DOM AwarenessYes (follows the cascade and inheritance)No (lexical block scoping, ignored by DOM)
JavaScript AccessYes (read/write dynamically at runtime)No (variables are destroyed after compiling)
Media Query SupportYes (values can change inside @media blocks)No (cannot redefine variable inside media query)
Fallback ValuesYes (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 via style.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

  1. Sass/Less Variables = Build-Time: Perfect for internal preprocessor structures, math, configure-once constants, and static library themes.
  2. CSS Custom Properties = Run-Time: Essential for interactive UI, dynamic configurations, user-customizable color schemes, and system-level themes.
  3. Inheritance Rules: Custom properties cascade down. Redefining a variable in .dark-card only affects elements inside .dark-card.
  4. JS Interoperability: Retrieve values using getComputedStyle(el).getPropertyValue('--name') and modify them using el.style.setProperty('--name', 'value').
  5. Always Provide Fallbacks: Ensure robust UI design by using the fallback parameter var(--color, #ccc) to guard against missing variables.

Finished practicing this challenge?

Mark it as completed to track your progress, or bookmark it to review later.

Loading...

Share this Resource

Help other developers level up by sharing this study guide.

⚡ Weekly newsletter

Crack Your Next Frontend Interview.

Join senior engineers who receive practical, deep-dive frontend challenges, detailed concepts, and blueprints directly in their inbox.

  • Senior level React, JS, and CSS interview blueprints
  • System Design & performance optimization deep-dives
  • 100% free, zero spam, unsubscribe with one click

Join the Study Track

We value your privacy. Unsubscribe at any time.

More Technical Questions

Expand your mastery. Deep dive into other frontend interview challenges in this category.