Toolmingo
Guides6 min read

CSS Variables (Custom Properties): Complete Guide with Examples

Master CSS custom properties — declare, use, scope, and update CSS variables with JavaScript. Includes dark mode, theming patterns, and a complete cheat sheet.

CSS custom properties (commonly called CSS variables) let you store values in one place and reuse them throughout your stylesheet. Change a single variable and the update cascades everywhere it is used — no find-and-replace, no Sass required.

This guide covers everything: declaration, scope, fallbacks, dark mode, JavaScript integration, and the pitfalls that trip up developers new to the feature.


Quick reference

Task Syntax
Declare a variable --color-primary: #3b82f6;
Use a variable color: var(--color-primary);
Fallback value color: var(--color-primary, blue);
Nested fallback color: var(--custom, var(--default, black));
Global scope :root { --size: 1rem; }
Component scope .card { --card-bg: white; }
Update via JS el.style.setProperty('--color', 'red');
Read via JS getComputedStyle(el).getPropertyValue('--color');
Use in calc() width: calc(var(--cols) * 100px);
Remove (inherit from parent) el.style.removeProperty('--color');

Declaring and using variables

All custom property names start with --. You declare them like any CSS property and use them with var().

:root {
  --color-primary: #3b82f6;
  --color-text: #1f2937;
  --spacing-md: 1rem;
  --radius: 0.5rem;
}

.button {
  background-color: var(--color-primary);
  color: white;
  padding: var(--spacing-md) calc(var(--spacing-md) * 1.5);
  border-radius: var(--radius);
}

:root is the highest-level element in the document — equivalent to html but with higher specificity. Declaring variables there makes them available everywhere.


Fallback values

var() accepts a second argument as fallback in case the variable is not defined:

.card {
  /* Use --card-bg if set; otherwise use white */
  background: var(--card-bg, white);
  color: var(--card-text, var(--color-text, #333));
}

Fallbacks can be chained: var(--a, var(--b, hardcoded)). The browser stops at the first defined value.

Gotcha: The fallback is used when the variable is undefined or invalid. An empty string (--color: ;) counts as defined, so the fallback is not used.


Scope and inheritance

CSS variables obey the cascade and inherit down the DOM tree. A variable declared on a parent is visible to all its descendants.

:root       { --color: blue; }      /* global */
.sidebar    { --color: green; }     /* overrides for .sidebar and children */
.sidebar a  { color: var(--color); } /* gets green */
.main a     { color: var(--color); } /* gets blue */

This scoping is what makes component theming clean:

/* Default card */
.card {
  --card-bg: #ffffff;
  --card-border: #e5e7eb;
  background: var(--card-bg);
  border: 1px solid var(--card-border);
}

/* Dark variant — just override the two variables */
.card--dark {
  --card-bg: #1f2937;
  --card-border: #374151;
}

Dark mode

The most common use of CSS variables is theming, especially dark/light mode:

:root {
  --bg: #ffffff;
  --text: #1f2937;
  --accent: #3b82f6;
}

@media (prefers-color-scheme: dark) {
  :root {
    --bg: #0f172a;
    --text: #f1f5f9;
    --accent: #60a5fa;
  }
}

body {
  background: var(--bg);
  color: var(--text);
}

a {
  color: var(--accent);
}

For a user-toggled dark mode (e.g. a button that switches themes), apply the dark-mode variables to a class on <html>:

:root {
  --bg: #ffffff;
  --text: #1f2937;
}

html.dark {
  --bg: #0f172a;
  --text: #f1f5f9;
}
document.querySelector('#toggle').addEventListener('click', () => {
  document.documentElement.classList.toggle('dark');
});

Updating variables with JavaScript

Unlike standard CSS properties, custom properties must be set with setProperty:

const root = document.documentElement;

// Set
root.style.setProperty('--color-primary', '#10b981');

// Read
const value = getComputedStyle(root).getPropertyValue('--color-primary').trim();
// → '#10b981'

// Remove (reverts to stylesheet declaration)
root.style.removeProperty('--color-primary');

This makes live theming trivial — store the user's colour choice in localStorage and apply it on page load:

const saved = localStorage.getItem('accent-color') ?? '#3b82f6';
document.documentElement.style.setProperty('--accent', saved);

Using variables in calc()

Variables work inside calc(), giving you dynamic calculations:

:root {
  --columns: 3;
  --gap: 1rem;
}

.grid {
  display: grid;
  grid-template-columns: repeat(var(--columns), 1fr);
  gap: var(--gap);
}

.full-width {
  /* Span all columns minus the gaps between them */
  width: calc(var(--columns) * 100px + (var(--columns) - 1) * var(--gap));
}

Note: var() inside calc() must resolve to a <number> or <length> — you cannot do calc(var(--cols)) when --cols: 3 and expect it to work as a multiplier alone. It works for lengths: calc(var(--size) * 2) where --size: 1rem.


Design tokens pattern

Large design systems store all primitive values as variables and derive semantic tokens from them:

/* Primitives */
:root {
  --blue-400: #60a5fa;
  --blue-600: #2563eb;
  --gray-900: #111827;
  --gray-50:  #f9fafb;
  --space-4:  1rem;
  --space-6:  1.5rem;
  --radius-md: 0.375rem;
}

/* Semantic tokens */
:root {
  --color-brand:    var(--blue-600);
  --color-brand-hover: var(--blue-400);
  --color-surface:  var(--gray-50);
  --color-on-surface: var(--gray-900);
  --spacing-component: var(--space-4);
  --border-radius:  var(--radius-md);
}

Semantic tokens describe intent (--color-brand) rather than value (#2563eb). Changing a theme only requires updating the semantic layer.


Animating with variables

CSS transitions and animations work with var() when the underlying type is animatable:

.box {
  --scale: 1;
  transform: scale(var(--scale));
  transition: transform 0.2s ease;
}

.box:hover {
  --scale: 1.05;
}

For more complex animations, update the variable from JavaScript during scroll or pointer events:

window.addEventListener('mousemove', (e) => {
  const x = (e.clientX / window.innerWidth) * 100;
  const y = (e.clientY / window.innerHeight) * 100;
  document.documentElement.style.setProperty('--mouse-x', `${x}%`);
  document.documentElement.style.setProperty('--mouse-y', `${y}%`);
});
.spotlight {
  background: radial-gradient(
    circle at var(--mouse-x, 50%) var(--mouse-y, 50%),
    rgba(255,255,255,0.15),
    transparent 60%
  );
}

6 common mistakes

1. Forgetting the double dash -color-primary is not a custom property. It must be --color-primary. The double dash is required by the spec.

2. Using a variable before declaring it Variables cascade down, not up. A variable on a child element is not visible to its parent.

.parent { color: var(--text); }  /* undefined — .child hasn't set it */
.child  { --text: red; }

3. Expecting variables to work in media query conditions You cannot use variables inside @media rules as the breakpoint value:

/* ❌ Does NOT work */
:root { --bp: 768px; }
@media (min-width: var(--bp)) { ... }

Use a preprocessor (Sass/PostCSS) or duplicate the value if you need named breakpoints in media queries.

4. Whitespace in values --spacing: 1 rem; (space before rem) is valid CSS but 1 rem is not a valid <length> — it will be treated as an invalid value and the fallback (if any) will be used. Always write 1rem without space.

5. Ignoring browser support for older environments CSS custom properties have excellent modern browser support (97 %+) but are not supported in IE11. If you must support IE11, use PostCSS with postcss-custom-properties to compile variables to static values at build time.

6. Overriding :root variables with inline styles element.style.setProperty('--x', 'y') sets the variable on that element, not on :root. If you intend to update the global value, target document.documentElement.


FAQ

Can I use CSS variables inside SVG? Yes — inline SVGs embedded in HTML inherit CSS custom properties from the document. External SVGs loaded via <img> or background-image do not.

Do CSS variables work in @keyframes? You can reference variables inside @keyframes, but the variable's value is resolved at the time the animation begins (not per frame). To animate the variable itself smoothly, use @property (Houdini) to register a typed custom property with a transition.

How are CSS variables different from Sass variables? Sass variables ($color: red) are resolved at build time — the output CSS contains only static values. CSS custom properties (--color: red) exist in the browser at runtime, can be read/changed by JavaScript, respond to media queries, and inherit through the DOM. You can use both together.

Can I use a CSS variable as a property name? No. var(--prop): red; is invalid. Variables can only appear as values, not property names or at-rule keywords.

Are CSS variables case-sensitive? Yes. --Color and --color are two different custom properties.

How do I scope a variable to a shadow DOM component? Use :host inside the component's shadow root: :host { --button-bg: blue; }. Variables declared outside the shadow DOM do not pierce into it unless the component explicitly uses them via var().

Related tools

Keep reading

All Toolmingotools are free & run in your browser

No sign-up, no upload, no watermark. Your files never leave your device.

Browse all tools