CSS custom properties — commonly called CSS variables — let you define a value once and reuse it anywhere in your stylesheet. Instead of hunting through hundreds of lines of CSS to change a brand color, you change it in one place, and every rule that references it updates automatically.

If you've used a preprocessor like Sass, this might sound familiar — but native CSS variables are actually more powerful in one important way: they're live values the browser resolves at render time, which means they can be read and changed by JavaScript, and they respect the normal CSS cascade (so you can override a variable differently inside a specific component, media query, or theme).

Defining and Using a Variable

You declare a custom property with a name starting with two dashes, typically on the :root pseudo-class so it's available globally. You read it anywhere with the var() function.

:root {
  --primary-color: #2563eb;
  --spacing-md: 16px;
  --radius: 8px;
}

.btn {
  background: var(--primary-color);
  padding: var(--spacing-md);
  border-radius: var(--radius);
}

.card {
  border: 1px solid var(--primary-color);
  border-radius: var(--radius);
}

Both .btn and .card now pull their color and corner radius from the same two variables. Change --primary-color once on :root, and both rules update without touching either one directly.

Fallback Values

var() accepts an optional second argument used if the variable isn't defined, which is handy for components meant to be reused in places that might not set every variable.

.badge {
  /* Falls back to #999 if --badge-color was never set */
  background: var(--badge-color, #999);
}

Scoping Variables Locally

Variables aren't limited to :root — defining one on any selector scopes it to that element and its descendants, letting you override a global value for just one section of the page.

.danger-zone {
  --primary-color: #dc2626; /* overrides the global blue, locally, just here */
}

Any .btn or .card rendered inside an element with the class danger-zone now picks up red instead of blue, with zero changes to the .btn/.card rules themselves — the cascade handles it automatically.

A Practical Theming Example

:root {
  --primary-color: #2563eb;
  --text-color: #111827;
  --bg-color: #ffffff;
}

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

This three-variable setup is the foundation most sites use for theming — it's exactly what powers a light/dark mode toggle, since switching themes often just means reassigning these same variables to a different set of values, rather than rewriting any component CSS at all.

Because they're resolved live by the browser, custom properties are also readable and writable from JavaScript via element.style.setProperty() — useful for things like a user-adjustable accent color that updates the whole page instantly without reloading any stylesheet.