Skip to content

Fix: CSS Custom Properties (Variables) Not Working or Not Updating

FixDevs ·

Quick Answer

How to fix CSS custom properties not applying — wrong scope, missing fallback values, JavaScript not setting variables on the right element, and how CSS variables interact with media queries and Shadow DOM.

The Error

You define a CSS custom property (variable) but it has no effect:

:root {
  --primary-color: #3b82f6;
}

.button {
  background-color: var(--primary-color); /* Shows as transparent/initial */
}

Or a variable defined inside a component doesn’t apply to child elements. Or JavaScript updates a CSS variable but the UI doesn’t change:

document.documentElement.style.setProperty('--theme-color', '#ff0000');
/* Nothing changes visually */

Or the variable works in one browser but not another.

Why This Happens

CSS custom properties follow the cascade and inheritance rules — they are not globally available by default. Common failure causes:

  • Wrong scope — the variable is defined on a selector that doesn’t apply to the element using it. --color on .card is only available to .card and its descendants, not to siblings or parents.
  • Typo in variable namevar(--primaryColor) vs var(--primary-color). CSS variable names are case-sensitive.
  • Missing :root declaration — variables defined anywhere other than :root (or a parent element) are not globally available.
  • JavaScript setting property on wrong elementelement.style.setProperty('--var', value) sets it on that element only. Child elements inherit it; parent and sibling elements do not.
  • Overridden by specificity — a more specific rule sets the same variable to a different value, or !important on a fallback.
  • Browser doesn’t support CSS variables — IE11 has no support. All modern browsers (Chrome 49+, Firefox 31+, Safari 9.1+) do.
  • Shadow DOM isolation — CSS variables cross Shadow DOM boundaries (they are inherited), but only if the host element has them in scope.

Fix 1: Define Variables in the Correct Scope

/* Global — available everywhere */
:root {
  --primary: #3b82f6;
  --spacing-md: 1rem;
  --font-size-base: 16px;
}

/* Scoped — only available within .card and its descendants */
.card {
  --card-padding: 1.5rem;
  --card-border: 1px solid #e5e7eb;
}

.card-header {
  padding: var(--card-padding); /* ✓ Works — .card-header is a descendant of .card */
}

.other-element {
  padding: var(--card-padding); /* ✗ Undefined — not a descendant of .card */
}

Use :root for truly global variables:

:root {
  /* Design tokens — use everywhere */
  --color-primary: #3b82f6;
  --color-secondary: #10b981;
  --color-danger: #ef4444;

  --spacing-xs: 0.25rem;
  --spacing-sm: 0.5rem;
  --spacing-md: 1rem;
  --spacing-lg: 2rem;

  --radius-sm: 0.25rem;
  --radius-md: 0.5rem;
  --radius-lg: 1rem;

  --font-sans: 'Inter', system-ui, sans-serif;
  --font-mono: 'Fira Code', monospace;
}

Fix 2: Always Provide a Fallback Value

If a variable is undefined, var() resolves to its fallback — or to the initial value if no fallback is given (often transparent for colors, 0 for lengths):

.button {
  /* Without fallback — transparent if --primary is not defined */
  background-color: var(--primary);

  /* With fallback — uses #3b82f6 if --primary is not defined */
  background-color: var(--primary, #3b82f6);

  /* Chained fallback */
  background-color: var(--button-bg, var(--primary, #3b82f6));
}

Use fallbacks to make components self-contained:

/* Component defines its own defaults via fallback */
.card {
  background: var(--card-bg, var(--surface, white));
  border-radius: var(--card-radius, var(--radius-md, 0.5rem));
  padding: var(--card-padding, 1.5rem);
  color: var(--card-text, var(--text-primary, #111827));
}

/* Consumer can override any of these */
.dark .card {
  --card-bg: #1f2937;
  --card-text: #f9fafb;
}

Fix 3: Fix JavaScript Not Updating CSS Variables

element.style.setProperty() sets an inline style on that specific element. The variable is then available to that element and all its descendants:

// Sets --theme on the root — available globally
document.documentElement.style.setProperty('--theme-color', '#ff0000');

// Sets --card-bg on a specific element — only affects that element and children
const card = document.querySelector('.card');
card.style.setProperty('--card-bg', '#f0f9ff');

// Read a variable's computed value
const value = getComputedStyle(document.documentElement)
  .getPropertyValue('--theme-color')
  .trim();
console.log(value); // '#ff0000'

Common mistake — setting on the wrong element:

// Wrong — sets on the button, not globally
document.querySelector('button').style.setProperty('--primary', '#red');
// Other buttons still use the old value

// Correct — set globally on :root
document.documentElement.style.setProperty('--primary', '#red');
// All elements using var(--primary) update immediately

Theme switching with CSS variables:

function setTheme(theme) {
  const root = document.documentElement;

  if (theme === 'dark') {
    root.style.setProperty('--bg', '#111827');
    root.style.setProperty('--text', '#f9fafb');
    root.style.setProperty('--border', '#374151');
  } else {
    root.style.removeProperty('--bg');     // Removes inline override — reverts to :root CSS
    root.style.removeProperty('--text');
    root.style.removeProperty('--border');
  }
}

// Or use a data attribute (cleaner approach)
document.documentElement.setAttribute('data-theme', 'dark');
:root {
  --bg: white;
  --text: #111827;
}

:root[data-theme="dark"] {
  --bg: #111827;
  --text: #f9fafb;
}

Fix 4: Fix Variables in Media Queries

CSS custom properties cannot be used inside @media query conditions — only in property values:

/* Wrong — variables cannot be used in media query conditions */
:root { --breakpoint-md: 768px; }

@media (min-width: var(--breakpoint-md)) { /* ✗ Does not work */
  .container { max-width: 1200px; }
}

/* Correct — use variables in property values inside media queries */
@media (min-width: 768px) {
  :root {
    --font-size-base: 18px;   /* ✓ Override variable value inside media query */
    --spacing-md: 1.25rem;
  }

  .container {
    padding: var(--spacing-md); /* ✓ Uses the updated variable */
  }
}

Responsive design with CSS variables:

:root {
  --columns: 1;
  --gap: 1rem;
  --font-size-h1: 1.75rem;
}

@media (min-width: 640px) {
  :root {
    --columns: 2;
    --gap: 1.5rem;
    --font-size-h1: 2.25rem;
  }
}

@media (min-width: 1024px) {
  :root {
    --columns: 3;
    --gap: 2rem;
    --font-size-h1: 3rem;
  }
}

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

h1 { font-size: var(--font-size-h1); }

Fix 5: Fix Variables with Calc()

When using var() inside calc(), the units must be part of the variable or explicitly added:

:root {
  --base-size: 16;         /* Unitless number */
  --base-size-px: 16px;   /* With unit */
  --multiplier: 1.5;
}

.element {
  /* Wrong — unitless variable in calc without unit */
  font-size: calc(var(--base-size) * 1);  /* ✗ 16 is invalid — no unit */

  /* Correct — multiply unitless number by a unit */
  font-size: calc(var(--base-size) * 1px);  /* ✓ = 16px */

  /* Or use a variable that already includes the unit */
  font-size: calc(var(--base-size-px) * var(--multiplier));  /* ✓ = 24px */

  /* Correct — add/subtract values with units */
  width: calc(var(--sidebar-width, 250px) + 2rem);  /* ✓ */
}

Fix 6: Fix Variables in Tailwind CSS / CSS-in-JS

Tailwind v4 (CSS-based config):

/* In your @theme or CSS file */
@theme {
  --color-primary: #3b82f6;
  --color-primary-dark: #2563eb;
}

/* Use in arbitrary values */
/* class="bg-[var(--color-primary)]" */

Tailwind v3 (JavaScript config):

// tailwind.config.js
module.exports = {
  theme: {
    extend: {
      colors: {
        primary: 'var(--color-primary)',  // Reference CSS variable
      },
    },
  },
};
/* Define the variable */
:root { --color-primary: #3b82f6; }
<!-- Use the Tailwind class — resolves to var(--color-primary) -->
<div class="bg-primary text-white">...</div>

CSS Modules:

/* styles.module.css */
.button {
  background-color: var(--button-bg, var(--primary, #3b82f6));
  color: var(--button-text, white);
  padding: var(--button-padding, 0.5rem 1rem);
}
// React component — override variables via inline style
<button
  className={styles.button}
  style={{ '--button-bg': '#10b981', '--button-text': 'white' }}
>
  Custom Button
</button>

Pro Tip: Passing CSS variables via style prop in React works perfectly for theming individual components. TypeScript will complain because custom properties aren’t in CSSProperties — cast to React.CSSProperties or extend the type:

const style = { '--button-bg': '#10b981' } as React.CSSProperties;

Fix 7: Debug CSS Variable Resolution

Inspect variables in browser DevTools:

  1. Open DevTools → Elements panel.
  2. Select an element.
  3. Go to the Computed tab.
  4. Search for the variable name or look for properties using var().
  5. In the Styles panel, hover over a var() value — DevTools shows the resolved value.

Log all CSS variables on an element:

function getCSSVariables(element = document.documentElement) {
  const styles = getComputedStyle(element);
  const variables = {};

  for (const prop of styles) {
    if (prop.startsWith('--')) {
      variables[prop] = styles.getPropertyValue(prop).trim();
    }
  }

  return variables;
}

console.log(getCSSVariables()); // All :root variables
console.log(getCSSVariables(document.querySelector('.card'))); // Card-scoped

Check if a variable is defined:

const value = getComputedStyle(document.documentElement)
  .getPropertyValue('--primary-color')
  .trim();

if (!value) {
  console.warn('--primary-color is not defined');
}

Still Not Working?

Check for a typo — names are case-sensitive:

:root { --primaryColor: blue; }
.el { color: var(--primary-color); }  /* ✗ Different name — undefined */
.el { color: var(--primaryColor); }   /* ✓ Correct */

Check if a CSS preprocessor is eating your variables. Some Sass/Less configurations try to resolve var() at compile time, which fails. Ensure your preprocessor is configured to pass var() through unchanged:

// Sass — use #{} to interpolate carefully or just write plain CSS var()
.element {
  color: var(--primary); // ✓ Sass passes this through unchanged
}

Check for Shadow DOM. CSS variables do cross the Shadow DOM boundary (they are inherited), but only if the host element is inside the scope where the variable is defined. Variables defined inside a shadow root do not leak out to the light DOM.

For related CSS issues, see Fix: Tailwind Classes Not Applying and Fix: CSS Grid Not Working.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles