How do CSS custom properties differ from Sass/Less variables?

Intermediate12 min interview
Skills tested:
Explaining build-time substitution versus runtime resolutionKnowing custom properties inherit and Sass variables do notKnowing custom properties are readable and writable from JavaScriptExplaining why custom properties can be redefined per selector and media queryNaming the places a custom property cannot be used

Advertisement

🧩 Scenario

This distinction decides whether a feature is even possible. A dark mode toggle that flips a data attribute and restyles the entire application without a rebuild requires custom properties, because a Sass variable was compiled away months ago. Conversely a loop that generates forty utility classes from a spacing map requires Sass, because custom properties cannot generate selectors. Knowing which is which prevents building the wrong foundation.

Architecture Walkthrough

One Is Compiled Away, the Other Ships

A Sass or Less variable is a preprocessor construct. It exists only in the source file and is substituted textually during compilation. By the time the browser receives the CSS, $primary is gone and every place it appeared contains a literal colour value.

A CSS custom property is a real CSS property. --primary: #3b82f6 is a declaration that lives in the stylesheet the browser parses, participates in the cascade, and is resolved at computed-value time. It exists in devtools, it can be inspected, and it can change while the page is running.

That single difference produces every other one.

Custom Properties Inherit and Cascade

Because a custom property is a real property, it inherits from ancestor to descendant and it participates fully in the cascade. It can be declared on :root for a global value and then redeclared on any selector to override it for that subtree. It can be redeclared inside a media query, a @supports block, a :hover state, or a [data-theme] attribute selector, and everything downstream that references it picks up the new value automatically.

Sass variables do none of this. They are lexically scoped to the block they are declared in, they do not cascade, and they cannot be redeclared per selector in a way that affects descendants at runtime. A Sass variable inside a media query changes what the compiler emits for rules written after it in that block, which is a different mechanism entirely.

Custom Properties Are Live at Runtime

JavaScript can read a custom property with getComputedStyle(el).getPropertyValue('--x') and write one with el.style.setProperty('--x', value). Writing one restyles everything that references it, with no class swapping and no rule generation.

This is what makes them the foundation of theming, of state-driven styling such as a progress bar width or a dynamic accent colour, and of passing values from JavaScript into CSS without inline styles for every affected property. A Sass variable cannot participate: it has not existed since the build finished.

Where Custom Properties Cannot Be Used

The limits are worth knowing precisely, because they are the cases where Sass variables remain necessary.

A custom property cannot appear in a media query condition. @media (min-width: var(--bp)) does not work, because media conditions are evaluated before custom properties are resolved. Sass variables can, since they are substituted before parsing.

A custom property cannot be used in a selector, and cannot construct property names or generate rules. Sass loops and maps can generate forty utility classes from a spacing scale; custom properties can only supply values to rules that already exist.

Custom properties are also untyped by default. The value is stored as a token stream and is only validated when substituted, so an invalid value fails at use time rather than at declaration time, and the failure mode is the property becoming invalid-at-computed-value-time, which resolves to unset rather than falling back to the previous value. @property addresses this by letting you register a syntax, an initial value, and whether the property inherits, which also makes custom properties animatable.

Two smaller details: custom property names are case-sensitive, unlike normal CSS property names, so --Primary and --primary are different properties. And var() accepts a fallback as its second argument, var(--x, 1rem), which is the only guard against an undefined property.

Choosing

Use custom properties for anything that must vary at runtime: theme colours, spacing that changes per context, component state, values coming from JavaScript, and any token a consumer of your component library should be able to override.

Use Sass or Less variables for build-time work that CSS cannot express: media query breakpoint values, map iteration that generates classes, mixin arguments, and mathematical computation the preprocessor should resolve once rather than the browser resolving on every element.

A common and reasonable pattern is to keep both, with Sass variables defining the source-of-truth scale and emitting it as custom properties on :root, so the values are available at runtime while the generation of utilities stays at build time.


Key Code Explained

/* SASS: compiled away. The browser never sees $primary. */
$primary: #3b82f6;
$breakpoint-md: 768px;

.button {
  background: $primary; /* becomes background: #3b82f6 */
}

/* Sass CAN drive a media query condition */
@media (min-width: $breakpoint-md) {
  .layout { display: grid; }
}

/* Sass CAN generate rules from a map */
$spacing: (1: 4px, 2: 8px, 3: 16px);
@each $key, $value in $spacing {
  .mt-#{$key} { margin-top: $value; }
}
/* CUSTOM PROPERTIES: real declarations that ship and cascade */
:root {
  --primary: #3b82f6;
  --space: 1rem;
}

.button {
  background: var(--primary);
  padding: var(--space);
}

/* Redeclare per subtree — descendants update automatically */
.panel--danger {
  --primary: #dc2626; /* every var(--primary) inside now resolves to red */
}

/* Redeclare in a media query, a state, or an attribute selector */
@media (min-width: 768px) {
  :root { --space: 1.5rem; }
}
.card:hover {
  --shadow-opacity: 0.2;
}
[data-theme='dark'] {
  --primary: #60a5fa;
}

/* var() fallback is the only guard against an undefined property */
.widget {
  gap: var(--widget-gap, 0.5rem);
}

/* CANNOT be used in a media query condition */
@media (min-width: var(--bp)) {
  /* does not work: conditions resolve before custom properties */
}

/* Names are CASE-SENSITIVE, unlike normal properties */
:root {
  --Primary: red;
  --primary: blue; /* two different properties */
}

/* @property adds a type, an initial value, and animatability */
@property --angle {
  syntax: '<angle>';
  initial-value: 0deg;
  inherits: false;
}
.spinner {
  transition: --angle 300ms; /* animatable because it is registered */
}
// Custom properties are live: readable and writable at runtime
const root = document.documentElement;

const current = getComputedStyle(root).getPropertyValue('--primary').trim();
root.style.setProperty('--primary', '#dc2626');
// Every rule referencing var(--primary) restyles immediately.
// A Sass variable cannot participate — it stopped existing at build time.

The .panel--danger rule is the clearest single demonstration of the difference. One declaration changes a value for an entire subtree, and every rule referencing var(--primary) inside that subtree updates without being aware of the override. There is no preprocessor equivalent, because the substitution already happened and each rule holds a literal.

The JavaScript block is the other half of the argument. A theme toggle, a user-chosen accent colour, or a progress value driven by scroll position all reduce to setting one property, and nothing in a preprocessor can be reached from the running page.


Tradeoffs

CapabilitySass / Less variableCSS custom property
ResolvedAt build time, by the compilerAt computed-value time, by the browser
Present in shipped CSSNoYes
Inherits and cascadesNoYes
Redeclarable per selector or stateNoYes
Readable and writable from JavaScriptNoYes
Usable in a media query conditionYesNo
Can generate selectors or rulesYes, via loops and mapsNo
TypedYes, by the preprocessorOnly via @property
Case-sensitiveNoYes

What Interviewers Actually Check

  • Whether you lead with build-time substitution versus runtime resolution
  • Whether you know custom properties inherit and cascade while Sass variables do not
  • Whether you can show reading and writing one from JavaScript
  • Whether you can name the cases where a custom property will not work
  • Whether you treat the two as complementary rather than as competitors

Follow-Up Questions

  1. What does @property add beyond typing, and why does registration make a custom property animatable?
  2. What happens when a var() reference is invalid, and why is it not the same as the declaration simply being ignored?
  3. Why are custom properties resolved at computed-value time rather than at parse time, and what does that imply for inheritance?
  4. How would you expose a component library's design tokens so consumers can override them without a rebuild?
  5. What performance considerations arise from very heavy custom property use, and where does the cost actually appear?

Common Candidate Mistakes

  • Treating the two as different syntaxes for the same feature, which hides the fact that only one of them exists at runtime
  • Writing @media (min-width: var(--bp)) and expecting it to work, when media conditions are evaluated before custom properties resolve
  • Expecting a Sass variable to respond to a theme toggle or a JavaScript update, when it was substituted away during compilation
  • Forgetting that custom property names are case-sensitive, so --Primary and --primary are unrelated properties
  • Assuming the browser validates a custom property's value at declaration time, when it is stored as an untyped token stream and only fails at substitution unless registered with @property

Interview Readiness Checklist

Before you leave this question, make sure you can answer:

  • Can you state when each is resolved and by what?
  • Can you explain the inheritance and cascade difference and why it matters for theming?
  • Can you read and write a custom property from JavaScript?
  • Can you name two places a custom property cannot be used?
  • Can you decide which mechanism to use for a theme colour and for a breakpoint value?

Summary

A Sass or Less variable is a preprocessor construct substituted textually at build time; the browser never sees it. A CSS custom property is a real CSS property that ships in the stylesheet, participates in the cascade, inherits to descendants, and is resolved at computed-value time. Every other difference follows from that.

Because custom properties are real properties, they can be declared globally on :root and redeclared on any selector, state, attribute, or media query, and every rule referencing them downstream updates automatically. They are also readable and writable from JavaScript via getPropertyValue and setProperty, which is what makes runtime theming, user-chosen accent colours, and state-driven styling possible without class swapping. A Sass variable cannot take part in any of that.

The limits mark out where preprocessors still earn their place. Custom properties cannot appear in a media query condition, cannot construct selectors or generate rules, are untyped unless registered with @property, and are case-sensitive. So use custom properties for anything that varies at runtime and anything a consumer should be able to override, use Sass for breakpoint values, map iteration, and build-time generation, and consider the common hybrid where Sass defines the source scale and emits it as custom properties on :root.

Frequently Asked Questions

Should a project use both?

Often yes. Sass variables suit build-time constants used in map iteration or media query construction; custom properties suit anything that must change at runtime, such as theme colours and component state.

Can a Sass variable be used inside a media query condition?

Yes, because it is substituted before the CSS is parsed. A custom property cannot, since media query conditions are evaluated before custom properties resolve.

Advertisement


Stay Updated

Get the latest frontend challenges, interview questions and tutorials delivered to your inbox.

Advertisement