Using CSS Custom Properties for Shared Design Values

In Brief
Use a CSS custom property when you need it in the cascade, inheritance, or there will be a runtime change; keep a Sass variable for work completed during compilation. In 2019, Internet Explorer did not support custom properties, and the fallback inside var() did not change that. A separate static declaration was still needed where Internet Explorer support mattered.
A Sass variable is useful when a value can be decided as the stylesheet is compiled. It becomes less useful when the browser needs to choose that value from the element's context.
Consider a notice component whose accent colour is blue by default but brown inside a checkout summary. Duplicating the whole component selector would work, but it would hide a simple relationship amongst repeated declarations. CSS custom properties let the accent take part in the cascade instead.
The Limitation of Compile‑Time Values
A Sass variable is resolved before the browser receives the stylesheet:
$notice-accent: #2457a6;.notice { border-left: 0.25rem solid $notice-accent;}The resulting CSS contains the colour itself. There is no $notice-accent for an element to inherit, and changing the Sass source in developer tools cannot recompile the file.
That is not a flaw in Sass. Sass variables remain good at calculations, generating selectors, and keeping build‑time settings in one place. The question is whether the value belongs to compilation or to the page the browser is resolving.
Declaring and Reading a Custom Property
A custom property's name begins with two hyphens. The var() function substitutes its value into another declaration:
:root { --space-unit: 0.5rem; --notice-accent: #2457a6;}.notice { border-left: 0.25rem solid var(--notice-accent, #2457a6); padding: calc(var(--space-unit) * 2);}The second argument to var() is a fallback. It is used when the named custom property is missing or has the guaranteed‑invalid value. It is not a browser‑support fallback, because a browser which does not understand custom properties does not understand var() either.
A fallback can contain another custom property, but that property needs its own usable value. This declaration still fails when both names are undefined:
.notice { border-color: var(--notice-accent, var(--missing-accent));}There is another less obvious edge. A custom property can contain almost any sequence of tokens, so --notice-accent: 1rem is valid as a custom‑property declaration. It only becomes unsuitable when substituted into border-color. At that point the declaration is invalid at computed‑value time; the browser does not go back to an earlier border-color declaration. The CSS Custom Properties specification defines this computed‑value behaviour, which is why the values supplied to a property still need to be appropriate for it.
Scope is the Useful Part
Custom properties inherit by default. A root declaration can provide a site‑wide default, whilst a nearer ancestor can supply a value for one part of the page:
:root { --notice-accent: #2457a6;}.checkout-summary { --notice-accent: #8a3d00;}.notice--critical { --notice-accent: #a30000;}.notice { border-left: 0.25rem solid var(--notice-accent, #2457a6);}A normal notice inherits blue from :root. A notice inside .checkout-summary inherits brown. A critical notice has its own red declaration, so that declared value wins over an inherited one.
The Cascade Still Applies
The browser does not treat a custom property as a separate global settings store. It chooses a declaration using the ordinary cascade, then inheritance supplies a value where the element has no winning declaration of its own.
This makes the placement important. Put a genuinely shared default on :root, but keep a component value with the closest owner when the rest of the page should not know about it. The earlier article on managing design values in front‑end code covers the judgement behind deciding what should be shared.
A component can also expose a deliberate point of variation without placing every internal measurement on :root. For example, a card could read --card-gap with a literal fallback whilst a compact container supplies a smaller value. That makes the container responsible for the contextual decision and leaves unrelated cards alone. It is still worth keeping the number of such openings small: if every declaration is externally adjustable, the component becomes harder to understand rather than easier to reuse.
Specificity and source order matter when two matching rules declare the same custom property, just as they do for ordinary declarations. Inheritance is considered after the cascade has established that the element has no winning declaration of its own. That distinction explains why .notice--critical above does not inherit the checkout value even though it remains inside the checkout.
Custom Properties and Sass Variables are Different Tools
The Sass documentation describes variables as a way to reuse values during stylesheet construction. CSS custom properties remain in the generated CSS and are resolved by the browser.
| Question | Sass variable | CSS custom property |
|---|---|---|
| When is it resolved? | During compilation | When CSS values are computed |
| Does it use the CSS cascade? | No | Yes |
| Does it inherit through elements? | No | Yes, by default |
| Can JavaScript change it without recompiling? | No | Yes |
One does not need to replace the other. Sass can organise partials, produce repeated rules, and perform build‑time work. A custom property is the better fit when a relationship must survive into the browser. Keeping that boundary clear is more useful than converting every literal in a stylesheet.
There are also jobs a custom property cannot do. var() supplies all or part of a property value; it cannot create a selector, a property name, or the condition in a media query. Those remain questions for ordinary CSS structure or for a preprocessor at build time. The browser‑resolved value is powerful precisely because its boundary is clear.
Changing a Value at Runtime
Runtime mutation should represent a real state rather than an ornamental theme switch. A progress indicator is a reasonable example because the value is already part of the component's state:
<div class="progress" role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="0"> <span class="progress__bar"></span> <span class="progress__label">0% complete</span></div>.progress { --completion: 0%;}.progress__bar { width: var(--completion);}function updateProgress(progress, value) { progress.style.setProperty('--completion', value + '%'); progress.setAttribute('aria-valuenow', value); progress.querySelector('.progress__label').textContent = value + '% complete';}The bar width is not the only expression of the state. The text and aria-valuenow change too, so the update does not depend on colour or visual width alone.
Supporting Browsers Without Custom Properties
At the time of writing, custom properties are available in current Chrome, Firefox, Safari, and Edge, but not Internet Explorer 11. If Internet Explorer must retain the component's basic appearance, put a usable static declaration first:
.notice { border-left: 0.25rem solid #2457a6; border-left-color: var(--notice-accent, #2457a6);}Internet Explorer ignores the declaration containing var() and keeps the first colour. Supporting browsers use the later declaration and gain the scoped override. This only works where the static value is an acceptable reduced behaviour; it cannot reproduce inheritance or runtime mutation.
Test both paths. In a supporting browser, inspect the computed value for the root default, the checkout override, and the critical notice. In Internet Explorer, confirm that the static declaration survives and that the interface remains understandable. Autoprefixer cannot recreate the cascade and runtime behaviour in a browser which lacks the feature.
Wrapping Up
CSS custom properties earn their place when the browser needs to resolve a relationship. Declare them at the narrowest sensible scope, remember that var() fallback has precise limits, and keep an explicit static value when an unsupported browser is part of the requirement. Sass still owns compilation; custom properties carry selected decisions into the page.
Postscript
Aug 2026: This article forms part of an archive restored from a previous version of my website. Its original publication date is accurate. During the restoration, I reviewed and updated it where appropriate for formatting, imagery, broken links, code correctness, and current internal references, whilst preserving the original technical context and intent.
CSS custom properties are now broadly supported across modern browsers. This article reflects the browser landscape at the time it was originally published, where Internet Explorer 11 remained an important compatibility consideration for many projects.