Home JavaScript CSS Variables and Custom Properties Explained — How to Write Smarter, Reusable Styles

CSS Variables and Custom Properties Explained — How to Write Smarter, Reusable Styles

In Plain English 🔥
Imagine you're painting every room in your house the same shade of blue. You mix that exact blue once, pour it into a labeled tin called 'Brand Blue', and every painter grabs from that same tin. If your client changes their mind and wants green instead, you swap out one tin — and every room updates instantly. CSS variables work exactly like that labeled tin: you define a value once, give it a name, and every style rule that references that name updates automatically when you change it.
⚡ Quick Answer
Imagine you're painting every room in your house the same shade of blue. You mix that exact blue once, pour it into a labeled tin called 'Brand Blue', and every painter grabs from that same tin. If your client changes their mind and wants green instead, you swap out one tin — and every room updates instantly. CSS variables work exactly like that labeled tin: you define a value once, give it a name, and every style rule that references that name updates automatically when you change it.

Every professional website you've ever used — Spotify, GitHub, your bank's app — has a consistent look. The buttons are always the same color, the fonts match everywhere, and spacing feels uniform. That consistency doesn't happen by accident, and it definitely doesn't happen by copy-pasting the same hex code 200 times across a stylesheet. There's a smarter system underneath, and CSS Variables are a big part of it.

Before CSS Variables existed, developers faced a painful problem: if a designer changed the brand color from #3a86ff to #6c63ff, the developer had to hunt through thousands of lines of CSS and update every single occurrence manually. Miss one and the site looks broken. It was tedious, error-prone, and made maintaining a design system a nightmare. CSS Custom Properties — the official name for CSS Variables — were introduced specifically to kill that problem dead.

By the end of this article you'll know how to declare your own CSS variables, use them across your entire stylesheet, change them on the fly with JavaScript, and understand exactly how they cascade and inherit. You'll also know the traps that catch beginners off guard and be ready to answer the questions interviewers love to ask about this topic.

Declaring and Using Your First CSS Variable

A CSS variable has two parts: a declaration and a usage. Think of the declaration as writing a word in a dictionary ('Brand Blue means #3a86ff') and the usage as pointing to that dictionary entry instead of writing the hex code directly.

Declarations always start with two dashes: --my-variable-name. That double-dash is not optional — it's what tells the browser 'this is a custom property, not a built-in CSS property'. You almost always declare your variables inside a :root selector. The :root selector targets the very top of your HTML document (the html element), which means variables declared there are available everywhere on the page — they're global, like a dictionary everyone can read.

To use a variable you wrap its name inside var(): color: var(--brand-color). That's it. The browser looks up --brand-color, finds the value, and uses it. Now if you ever change the declaration, every single rule using that variable updates automatically. No find-and-replace, no risk of missing a spot.

brand-styles.css · CSS
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748
/* ── Step 1: Declare your variables inside :root ──────────────────────
   :root is like the global dictionary for your whole page.
   Every selector below can read these values. */
:root {
  --brand-color: #3a86ff;        /* primary blue used for buttons, links, highlights */
  --brand-color-dark: #1a56cc;   /* darker shade for hover states */
  --text-color: #1a1a2e;         /* near-black for body text — easier on eyes than pure black */
  --background-color: #f8f9ff;   /* very light blue-white page background */
  --spacing-unit: 16px;          /* base spacing — multiply this for consistent rhythm */
  --border-radius: 8px;          /* rounded corners used on cards, buttons, inputs */
  --font-family-base: 'Segoe UI', Arial, sans-serif;
}

/* ── Step 2: Use variables with var() wherever you need them ───────────
   Instead of typing #3a86ff over and over, we just reference the variable. */
body {
  background-color: var(--background-color); /* grabs #f8f9ff from :root */
  color: var(--text-color);                  /* grabs #1a1a2e from :root */
  font-family: var(--font-family-base);
  margin: 0;
  padding: var(--spacing-unit);              /* 16px padding around the page */
}

.btn-primary {
  background-color: var(--brand-color);      /* #3a86ff — matches the :root declaration */
  color: #ffffff;
  padding: calc(var(--spacing-unit) / 2) var(--spacing-unit); /* 8px top/bottom, 16px left/right */
  border: none;
  border-radius: var(--border-radius);       /* 8px rounded corners */
  cursor: pointer;
  font-size: 1rem;
  transition: background-color 0.2s ease;
}

/* ── Hover state — only ONE value changes, not a whole new color string ── */
.btn-primary:hover {
  background-color: var(--brand-color-dark); /* swaps to #1a56cc on hover */
}

.card {
  background-color: #ffffff;
  border: 1px solid var(--brand-color);      /* consistent branded border */
  border-radius: var(--border-radius);
  padding: var(--spacing-unit);              /* 16px padding inside card */
  margin-bottom: var(--spacing-unit);
}

/* ── Result: Change --brand-color in :root and EVERY rule above updates ── */
▶ Output
Visual result: Page has a light blue-white background. The .btn-primary button appears blue (#3a86ff) with white text, 8px rounded corners, and 8px/16px padding. On hover, it darkens to #1a56cc. The .card has a blue border and consistent spacing — all driven by the six variables declared in :root.
🔥
Why the double dash?The -- prefix is intentional. It tells the browser 'this is a custom property defined by the author, not a native CSS property'. Without it, the browser would try to interpret it as a built-in property and ignore it silently — no error, just nothing working. Always use --.

How CSS Variables Inherit and Scope — This Changes Everything

Here's the part that trips up most beginners: CSS variables follow the same inheritance rules as regular CSS properties like color and font-size. That means a variable declared on a parent element is automatically available to all of its children. And you can override a variable for just one section of the page by declaring it on a specific element — without touching any other section.

This is called scoping, and it's one of the most powerful features of CSS variables. Think of it like a company with a global dress code (blue shirts for everyone), but one department is allowed to wear red shirts instead. The global rule applies everywhere, except where a local rule overrides it.

A classic real-world use case: dark mode. Instead of writing an entirely separate dark-mode stylesheet, you just flip the values of your color variables on the body element (or a data-theme attribute), and every child element that uses those variables automatically gets the new look — no extra CSS rules needed for individual components.

scoped-variables.css · CSS
1234567891011121314151617181920212223242526272829303132333435363738394041424344
/* ── Global variables on :root — available to the ENTIRE page ─────────── */
:root {
  --card-bg: #ffffff;          /* white card background by default */
  --card-text: #1a1a2e;        /* dark text by default */
  --card-border: #e0e0ef;      /* light grey border by default */
  --highlight-color: #3a86ff;  /* blue accent color */
}

/* ── The .card component uses the variables — no hard-coded colors ─────── */
.card {
  background-color: var(--card-bg);
  color: var(--card-text);
  border: 1px solid var(--card-border);
  padding: 20px;
  border-radius: 8px;
  margin: 12px 0;
}

.card h2 {
  color: var(--highlight-color); /* uses the global blue accent */
  margin: 0 0 8px 0;
}

/* ── Dark theme section — scoped override on a specific container ──────
   Any .card INSIDE .dark-section automatically gets dark styles.
   Cards OUTSIDE .dark-section are completely unaffected. */
.dark-section {
  /* We re-declare the SAME variable names with new values.
     Children inherit these new values instead of the :root ones. */
  --card-bg: #1a1a2e;          /* dark background for cards in this section */
  --card-text: #e0e0ff;        /* light text so it's readable on dark bg */
  --card-border: #3a3a5c;      /* subtle dark border */
  --highlight-color: #ff6b6b;  /* swap to red accent for dark section */

  background-color: #0d0d1a;   /* the section itself is very dark */
  padding: 24px;
  border-radius: 12px;
}

/* ── A fallback value — the second argument to var() is a safety net ───
   If --card-shadow is never declared, the browser uses 'none' instead of breaking. */
.card {
  box-shadow: var(--card-shadow, none); /* fallback = 'none' if --card-shadow doesn't exist */
}
▶ Output
Cards outside .dark-section: white background, dark text, blue heading, light grey border.
Cards INSIDE .dark-section: dark navy background, light lavender text, red heading, dark border.
The .card CSS rules are identical — only the variable values changed via scoping. Zero duplication.
⚠️
The Fallback Value Is Your Safety Netvar(--my-variable, fallback-value) — that second argument after the comma is what the browser uses if the variable isn't defined. Always add a fallback for variables that might not exist in all contexts. Example: var(--card-shadow, 0 2px 8px rgba(0,0,0,0.1)). This prevents invisible layout bugs when a variable name has a typo or is missing in a certain scope.

Updating CSS Variables with JavaScript for Live, Dynamic Styles

This is where CSS Variables go from 'nice convenience' to 'genuinely powerful tool'. Because CSS variables are part of the DOM's computed style, JavaScript can read and write them in real time. When you update a variable's value via JavaScript, every single element using that variable re-renders immediately — no class swapping, no inline style spaghetti, no page reload.

You update a CSS variable in JavaScript using element.style.setProperty('--variable-name', 'new-value'). To update a global variable (one declared on :root), you target document.documentElement — that's the html element, which is what :root refers to in CSS.

You can also read the current value of a CSS variable using getComputedStyle(element).getPropertyValue('--variable-name'). This is useful for building things like color pickers, theme switchers, or sliders that let users customize the look of your app in real time — all without writing a single line of extra CSS.

dynamic-theme-switcher.js · JAVASCRIPT
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576
// ── HTML structure this script expects ─────────────────────────────────
// <div id="theme-controls">
//   <button id="toggle-dark-mode">Toggle Dark Mode</button>
//   <input type="range" id="font-size-slider" min="14" max="24" value="16" />
//   <input type="color" id="accent-color-picker" value="#3a86ff" />
// </div>
// <p id="current-font-size">Current font size: 16px</p>

// ── document.documentElement is the <html> element — same as :root in CSS ──
const rootElement = document.documentElement;

// ── Helper: update a CSS variable on :root from JavaScript ─────────────
function setCSSVariable(variableName, value) {
  // setProperty takes the variable name (with --) and the new value as strings
  rootElement.style.setProperty(variableName, value);
  console.log(`CSS variable ${variableName} updated to: ${value}`);
}

// ── Helper: read the current value of a CSS variable ───────────────────
function getCSSVariable(variableName) {
  // getComputedStyle reads the FINAL computed value, including :root declarations
  const value = getComputedStyle(rootElement).getPropertyValue(variableName);
  return value.trim(); // trim() removes any accidental whitespace
}

// ── Feature 1: Dark mode toggle ─────────────────────────────────────────
const darkModeButton = document.getElementById('toggle-dark-mode');
let isDarkMode = false;

darkModeButton.addEventListener('click', function () {
  isDarkMode = !isDarkMode; // flip the boolean flag

  if (isDarkMode) {
    // Override the global color variables for dark mode
    setCSSVariable('--background-color', '#1a1a2e');  // dark navy page background
    setCSSVariable('--text-color', '#e0e0ff');         // light lavender text
    setCSSVariable('--brand-color', '#ff6b6b');        // switch accent to warm red
    darkModeButton.textContent = 'Switch to Light Mode';
  } else {
    // Restore original light mode values
    setCSSVariable('--background-color', '#f8f9ff');  // light page background
    setCSSVariable('--text-color', '#1a1a2e');         // dark text
    setCSSVariable('--brand-color', '#3a86ff');        // back to blue accent
    darkModeButton.textContent = 'Switch to Dark Mode';
  }
});

// ── Feature 2: Live font-size slider ────────────────────────────────────
const fontSizeSlider = document.getElementById('font-size-slider');
const fontSizeDisplay = document.getElementById('current-font-size');

fontSizeSlider.addEventListener('input', function () {
  const newSize = this.value + 'px'; // e.g. '18px'

  // Update the CSS variable — every element using --base-font-size re-renders
  setCSSVariable('--base-font-size', newSize);

  // Also update the display so the user can see the current value
  fontSizeDisplay.textContent = `Current font size: ${newSize}`;
});

// ── Feature 3: Accent color picker ──────────────────────────────────────
const accentColorPicker = document.getElementById('accent-color-picker');

accentColorPicker.addEventListener('input', function () {
  // this.value is the hex color chosen by the user, e.g. '#ff6b6b'
  setCSSVariable('--brand-color', this.value);

  // Read it back to confirm — shows how getCSSVariable works
  const confirmedValue = getCSSVariable('--brand-color');
  console.log(`Brand color is now: ${confirmedValue}`);
});

// ── On page load: log the current value of our base variables ───────────
console.log('Initial brand color:', getCSSVariable('--brand-color'));      // #3a86ff
console.log('Initial background:', getCSSVariable('--background-color')); // #f8f9ff
▶ Output
Console on page load:
Initial brand color: #3a86ff
Initial background: #f8f9ff

When dark mode button is clicked:
CSS variable --background-color updated to: #1a1a2e
CSS variable --text-color updated to: #e0e0ff
CSS variable --brand-color updated to: #ff6b6b
→ Every element using those variables instantly re-renders in dark colors.

When slider moves to 20:
CSS variable --base-font-size updated to: 20px
→ All text using var(--base-font-size) grows live without a page reload.

When color picker selects #e63946:
CSS variable --brand-color updated to: #e63946
Brand color is now: #e63946
⚠️
Watch Out: setProperty vs Direct AssignmentNever do rootElement.style['--brand-color'] = '#ff0000' — that syntax doesn't work for custom properties in all browsers. Always use rootElement.style.setProperty('--brand-color', '#ff0000'). The setProperty method is the correct, spec-compliant way to write CSS custom properties from JavaScript.
Feature / AspectCSS Variables (Custom Properties)Hardcoded Values in CSS
Change one value, update everywhereYes — update the variable once, every reference updatesNo — must find and replace every occurrence manually
Theme switching (dark/light mode)Trivial — flip variable values on a parent elementRequires duplicating entire rule sets for each theme
JavaScript can read/write the valueYes — via setProperty and getPropertyValueNo — JS can only set inline styles, not find all usages
Inheritance and scopingInherits down the DOM tree, scoped to any elementNo concept of scope — each property stands alone
Browser supportAll modern browsers, no IE11 supportUniversal — works in every browser ever made
Fallback valuesBuilt-in: var(--name, fallback)N/A — the value is the value
Works inside calc()Yes — var() works inside calc() expressionsYes — raw values also work inside calc()
Readable from DevToolsYes — visible in Computed Styles panelYes — but harder to trace where the value came from

🎯 Key Takeaways

  • Always declare global CSS variables inside :root — that's the equivalent of a global dictionary your entire stylesheet can read from.
  • CSS variables inherit DOWN the DOM tree. Declaring a variable on a child element creates a scoped override — the parent and siblings are unaffected, but all descendants of that element see the new value.
  • Use var(--variable-name, fallback-value) whenever a variable might not exist in a given context. The fallback prevents invisible layout breakage from a missing or misspelled variable name.
  • From JavaScript, always use element.style.setProperty('--variable-name', value) to write and getComputedStyle(element).getPropertyValue('--variable-name') to read. Direct bracket assignment does not work reliably for custom properties.

⚠ Common Mistakes to Avoid

  • Mistake 1: Forgetting the double dash on the variable name — Writing -brand-color or brand-color instead of --brand-color. The browser silently ignores the declaration (no error, no warning) and falls back to the default or nothing at all. Fix it: always start custom property names with exactly two dashes (--). If your style looks mysteriously broken, open DevTools, click the element, check the Styles panel — a misspelled variable will show as invalid or unset.
  • Mistake 2: Trying to build a variable name dynamically inside var() — Writing something like var(--color-#{theme}) and expecting it to work. CSS is not a programming language — it can't evaluate expressions inside var(). Fix it: do the dynamic part in JavaScript instead. Use JS to determine the full variable name as a string and then call setProperty with that complete name.
  • Mistake 3: Declaring variables on a specific element and wondering why siblings or parents can't see them — CSS variables only flow DOWN the DOM tree through inheritance. If you declare --accent-color on a .sidebar div, a .header div at the same level can't access it. Fix it: declare shared variables on :root (or the nearest common ancestor of all elements that need them). Only declare variables on specific elements when you intentionally want that scope to be limited.

Interview Questions on This Topic

  • QWhat is the difference between a CSS custom property (CSS variable) and a Sass/LESS variable — and when would you choose one over the other?
  • QHow would you implement a live dark-mode toggle using CSS variables and JavaScript, without writing duplicate CSS rule sets for each theme?
  • QIf a CSS variable is declared on a div with the value red, and a child span inside that div also has the same variable declared with the value blue, what color will a grandchild element inside the span see — and why?

Frequently Asked Questions

What is the difference between a CSS variable and a Sass variable?

A Sass variable (like $brand-color) is processed at build time — it's replaced with its value before the CSS file is ever sent to the browser, so it can't change at runtime. A CSS custom property (like --brand-color) lives in the browser and can be read, updated, and scoped dynamically using JavaScript or CSS inheritance. Sass variables are compile-time constants; CSS variables are runtime values. For theming and interactivity, CSS variables win. For things like mixins and loops, Sass is still useful.

Do CSS variables work in all browsers?

CSS custom properties work in all modern browsers — Chrome, Firefox, Safari, Edge, and mobile browsers. The one notable exception is Internet Explorer 11, which has zero support. If you need IE11 support (rare in 2024), you must provide hardcoded fallback values alongside your var() usage or use a PostCSS plugin to polyfill them at build time.

Can I use CSS variables inside media queries or animations?

You can use CSS variables INSIDE the rules of a media query (e.g., redefining :root variables for mobile breakpoints), but you cannot use var() as the condition value of the media query itself — so @media (max-width: var(--breakpoint-md)) won't work. For animations (keyframes), CSS variables do work and can be animated if the browser can interpolate the value type, though animating color variables requires careful browser support testing.

🔥
TheCodeForge Editorial Team Verified Author

Written and reviewed by senior developers with real-world experience across enterprise, startup and open-source projects. Every article on TheCodeForge is written to be clear, accurate and genuinely useful — not just SEO filler.

← PreviousCSS Animations and TransitionsNext →Semantic HTML Explained
Forged with 🔥 at TheCodeForge.io — Where Developers Are Forged