scalpel@labs: ~/glossary/css-custom-properties.mdx6 sections

CSS Custom Properties (Variables) for Color

CSS custom properties are variables you declare with a `--` prefix, usually on `:root`, and read with `var()`: `:root { --brand: #38bdf8 }` then `color: var(--brand)`. They let one colour live in one place and cascade everywhere.

extension: Scalpel Colorupdated: 2026-08-14read_time: 2 min
less css-custom-properties.mdx

Why it matters

Repeat a hex code in forty rules and a rebrand becomes forty edits and one you will miss. A custom property makes it one edit. Because they cascade and can be overridden per component or per theme, they are also how most sites do dark mode without duplicating a stylesheet. The extension exports a scanned palette straight into this shape so you can paste a working :root block.

How it works

Declare a custom property on a selector with a -- prefix, then read it anywhere within that selector's scope using var():

:root {
  --brand: #38bdf8;
  --text: #1a202c;
  --surface: #ffffff;
}

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

.button-primary {
  background-color: var(--brand);
}

The custom property is resolved at runtime, not at compile time like a Sass variable. That means a component can override :root values on its own selector, and everything inside inherits the new value:

.card {
  --surface: #f8f9fa;     /* override just for cards */
  background: var(--surface);   /* uses #f8f9fa inside .card */
}

.card .button {
  background: var(--brand);  /* still uses #38bdf8 from :root */
}

Scope and cascade

Custom properties follow the cascade. Define on :root for global scope. Define on a component class for local overrides. Define in a media query or attribute selector for theme switching.

Dark mode is the classic case:

:root {
  --text: #1a202c;       /* light mode default */
  --surface: #ffffff;
}

@media (prefers-color-scheme: dark) {
  :root {
    --text: #f7fafc;     /* dark mode override */
    --surface: #1a202c;
  }
}

Every rule that uses var(--text) and var(--surface) adapts automatically. No duplicate selectors, no if-this-then-that cascade fighting.

What does not matter

Custom properties are not Sass variables. Sass variables are compile-time; they are gone by the time the browser sees the CSS. Custom properties are runtime data that the browser can see and change dynamically. That is more powerful for themes but also more verbose. Use the tool that fits the job.

The -- prefix is required. There is no $variable shorthand in vanilla CSS. And custom properties cannot be used outside CSS, though some build tools can read them from a separate JSON file and generate CSS.

Code example

A working theme system with light and dark modes:

:root {
  --color-primary: #3b82f6;
  --color-secondary: #8b5cf6;
  --color-text: #1f2937;
  --color-bg: #ffffff;
  --color-border: #e5e7eb;
}

@media (prefers-color-scheme: dark) {
  :root {
    --color-text: #f3f4f6;
    --color-bg: #111827;
    --color-border: #374151;
    /* primary and secondary stay the same */
  }
}

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

.button {
  background: var(--color-primary);
  border: 1px solid var(--color-border);
}

.button:hover {
  background: var(--color-secondary);
}

A fallback in case a variable is not defined:

color: var(--brand, #3b82f6);   /* uses #3b82f6 if --brand is missing */

How Scalpel Color shows it

On the Palette tab, the "CSS variables" export button generates a :root block with all scanned or saved colours declared as custom properties. Copy the output and paste it into your stylesheet. Each colour becomes --color-N or a name you provide. The extension also lets you view which custom properties are actually used on the current page in the CSS Colors tab.

Sources