Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

CSS  Module 14 – Custom properties, nesting, scope and architecture

Custom properties (CSS variables)

Declare CSS custom properties, use them with var() and fallbacks, theme components by overriding them, and read or set them from JavaScript.

  • Intermediate
  • 25 minutes
  • Examples run with Node.js 24.21.0
  • By MySmartCoPilot
Free lesson, Pro answers: 25 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Declare custom properties and use them with var() and fallbacks
  • Build themes and component variants by overriding custom properties through inheritance
  • Read and write custom properties from JavaScript

Before you start

A custom property is a property you name yourself, with two hyphens at the start: --brand: #0f766e;. On its own it changes nothing on the page. The var() function reads it into a real property, as in color: var(--brand), so one value can feed every colour, border and shadow that should match. Custom properties take part in the cascade and inherit like color does: a value set on :root reaches every element, and a value set on a component changes that component and everything inside it. That is how themes, design tokens and component variants are built without a preprocessor. They are not compiled away, so JavaScript can change them while the page runs. Custom properties, often called CSS variables, are Baseline widely available: Chrome supports them since version 49, Edge since 15, Firefox since 31 and Safari since 9.1. Each example below is a small page whose values this lesson’s cascade model works out and prints, and Chromium 153, Firefox 155 and WebKit 26.6 compute the same values for the same pages.

Declaring and reading a custom property

A custom property is declared like any other property: a name, a colon and a value, inside a rule. The name must start with --, and it is case-sensitive, so --brand and --Brand are two different properties. The value can be almost anything, because the browser does not check it when it reads the style sheet: --gap: 1rem 2rem, --shadow: 0 1px 2px rgb(0 0 0 / 0.2) and --label: "New" are all fine. It also keeps the value as written, spaces and capital letters included, until something uses it.

var(--name) can stand for a whole value or any part of one, in any property: border: 1px solid var(--brand) or padding: var(--space-2) var(--space-4). It cannot stand for anything else. The specification allows var() only in property values, so var(--side): 20px is a syntax error that the browser drops, a selector cannot contain one, and @media (min-width: var(--bp)) never matches in any of the three engines: a media query condition is not a property value.

Inheritance: one knob, many elements

Custom properties are inherited. A declaration on :root, the html element, gives every element on the page the same value, and a declaration on any element gives that element and its descendants a new value without touching the rest of the page. That is enough to build a component whose variants are nothing but new values for its knobs. In the style sheet below, one .btn rule draws every button from --btn-bg and --btn-fg. The variants set only those, and the .promo section sets only --brand:

One button rule, two variants and a themed section

buttons.css

/* buttons.css: one rule draws every button from a few knobs; variants and sections only turn the knobs. */
:root {
  --brand: #0f766e;
  --danger: #b91c1c;
  --on-brand: #ffffff;
}

.btn {
  --btn-bg: var(--brand);
  --btn-fg: var(--on-brand);
  background-color: var(--btn-bg);
  color: var(--btn-fg);
  border: 2px solid var(--btn-border, var(--btn-bg));
  padding: 8px 16px;
}

.btn--danger { --btn-bg: var(--danger); }
.btn--ghost { --btn-bg: transparent; --btn-fg: var(--brand); --btn-border: var(--brand); }

/* A campaign section changes the brand colour for everything inside it. */
.promo { --brand: #6d28d9; }

buttons.html

<html>
  <body>
    <main>
      <button class="btn">Save</button>
      <button class="btn btn--danger">Delete</button>
      <button class="btn btn--ghost">Cancel</button>
      <section class="promo">
        <button class="btn">Book a table</button>
        <button class="btn btn--ghost">See the menu</button>
      </section>
    </main>
  </body>
</html>
The computed colours of the five buttons JavaScript · buttons.mjs
// Five buttons drawn by the one .btn rule of buttons.css. The cascade model in ./css-model reads the page and the
// style sheet and prints the computed colours of each button: the variants and the .promo section set only custom
// properties, never background-color, color or border themselves.
import { readFileSync } from "node:fs";
import { createPage, describe } from "./css-model/index.mjs";

const page = createPage({
  html: readFileSync("buttons.html", "utf8"),
  sheets: [{ origin: "author", css: readFileSync("buttons.css", "utf8") }],
});

const properties = ["background-color", "color", "border-top-color"];
const rows = [["button", "inside", ...properties]];
for (const button of page.queryAll("button")) {
  rows.push([describe(button), describe(button.parent), ...properties.map((p) => page.computed(button, p))]);
}
const widths = rows[0].map((_, i) => Math.max(...rows.map((row) => row[i].length)));
for (const row of rows) console.log(row.map((cell, i) => cell.padEnd(widths[i])).join("  ").trimEnd());

Output

button                  inside         background-color  color    border-top-color
button.btn              main           #0f766e           #ffffff  #0f766e
button.btn.btn--danger  main           #b91c1c           #ffffff  #b91c1c
button.btn.btn--ghost   main           transparent       #0f766e  #0f766e
button.btn              section.promo  #6d28d9           #ffffff  #6d28d9
button.btn.btn--ghost   section.promo  transparent       #6d28d9  #6d28d9

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node buttons.mjs

The two buttons in the section turn purple, although no rule for them mentions purple. Each button declares --btn-bg: var(--brand) itself, so the var() is replaced on the button, with the --brand that the button inherited: #0f766e in main and #6d28d9 in the section. The ghost button’s border reads a third knob with a fallback, var(--btn-border, var(--btn-bg)), so variants that never set --btn-border get a border in their background colour.

This is the shape of a component’s public interface. The component declares its knobs and gives each a default, either a page-wide token such as var(--brand) or a literal value; pages and variants then override knobs, never the component’s real properties.

Derived values are worked out where they are declared

Nested elements from :root to two buttons and a link: --btn-bg follows the section's --brand, --link keeps the root value.:root--brand: #0f766e--link: var(--brand)so --link is #0f766emaininherits --brandand --link: #0f766ebutton.btn--btn-bg: var(--brand), so #0f766esection.promo--brand: #6d28d9inherits --link: #0f766ebutton.btn--btn-bg: var(--brand), so #6d28d9acolor: var(--link), so #0f766e

Each var() is replaced on the element that declares it, and children inherit the result

Text description of the diagram

The diagram shows elements as nested boxes, each inside its parent, with the custom properties that matter for each.

  1. :root declares --brand: #0f766e and --link: var(--brand). The var() is replaced here, so --link is #0f766e.
  2. Inside it, main inherits both: --brand and --link are #0f766e.
  3. Inside main, a button.btn declares --btn-bg: var(--brand). The var() is replaced on the button with the --brand it inherited, so --btn-bg is #0f766e.
  4. Also inside main, section.promo declares --brand: #6d28d9. It still inherits --link as #0f766e, the value worked out on :root.
  5. Inside the section, another button.btn declares --btn-bg: var(--brand) as well. Here --brand is #6d28d9, so --btn-bg is #6d28d9: the button follows the section.
  6. Also inside the section, a link uses color: var(--link). It inherited --link, which is still #0f766e, so the link does not follow the section's new brand colour.

A var() inside a custom property is replaced on the element that declares the property, and the result, not the var(), is what its children inherit. The specification says so directly: custom properties resolve their var() functions at computed-value time, which comes before inheritance. The first token file below declares --link: var(--brand) on :root. There, --brand is teal, so --link becomes #0f766e and is inherited as #0f766e. When the section sets a new --brand, nothing works --link out again:

A derived token on :root, and the fix

derived-before.css

/* The token file as it was: a derived token declared next to the token it is made from. */
:root {
  --brand: #0f766e;
  --link: var(--brand); /* worked out here, on :root, and inherited as #0f766e */
}
.promo { --brand: #6d28d9; }
a { color: var(--link); }

derived-after.css

/* The fix: declare the derived token wherever the token it is made from can change. */
:root { --brand: #0f766e; }
.promo { --brand: #6d28d9; }
:root, .promo { --link: var(--brand); } /* worked out again on .promo, with its own --brand */
a { color: var(--link); }

derived.html

<html>
  <body>
    <p>Read the <a class="guide">style guide</a>.</p>
    <section class="promo">
      <p>Festival menu: <a class="menu">see the dishes</a>.</p>
    </section>
  </body>
</html>
The links' colours with each token file JavaScript · derived.mjs
// The same page with two token files. The cascade model in ./css-model prints the colour of each link and the value
// of --link it inherited: a custom property's var() is replaced on the element that declares it, and the result is
// what its children inherit.
import { readFileSync } from "node:fs";
import { createPage } from "./css-model/index.mjs";

const html = readFileSync("derived.html", "utf8");
for (const file of ["derived-before.css", "derived-after.css"]) {
  const page = createPage({ html, sheets: [{ origin: "author", css: readFileSync(file, "utf8") }] });
  console.log(file);
  for (const selector of ["a.guide", ".promo a.menu"]) {
    const link = page.query(selector);
    console.log(`  ${selector.padEnd(13)} --brand ${page.computed(link, "--brand")}  --link ${page.computed(link, "--link")}  color ${page.computed(link, "color")}`);
  }
}

Output

derived-before.css
  a.guide       --brand #0f766e  --link #0f766e  color #0f766e
  .promo a.menu --brand #6d28d9  --link #0f766e  color #0f766e
derived-after.css
  a.guide       --brand #0f766e  --link #0f766e  color #0f766e
  .promo a.menu --brand #6d28d9  --link #6d28d9  color #6d28d9

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node derived.mjs

With the first file, the link in the section keeps the old colour although its own --brand is purple, and all three engines agree. The second file declares --link on every element where --brand can change, :root, .promo, so it is worked out again inside the section. A design system with many derived tokens and many themes can follow the rule the button follows instead: declare derived values on the components that use them, not on :root.

Fallbacks, and values that do not fit

The second argument of var() is a fallback, used when the custom property has no value: it was never declared, it was set to initial, or it is part of a cycle of properties that refer to each other. Everything after the first comma belongs to the fallback, so var(--font, Georgia, serif) falls back to Georgia, serif. A fallback can contain another var(), and an empty fallback, var(--x,), stands for nothing.

The fallback is not a type check. When the custom property holds a value that makes no sense for the property using it, the browser does not try the fallback. The declaration fails once the element’s values are computed, and the property then acts as unset: inherited if it is an inherited property, its initial value otherwise. The program below runs nine declarations on a paragraph with the default 16px font size:

What nine var() declarations compute to JavaScript · fallbacks.mjs
// Nine declarations that use var(), each on a paragraph whose parent has the default 16px font size. The cascade
// model in ./css-model prints what each declaration computes to: the fallback after the comma is used only when the
// custom property has no value at all.
import { createPage } from "./css-model/index.mjs";

const cases = [
  ["--size is never declared", "font-size: var(--size, 2rem);", "font-size"],
  ["--size holds a colour", "--size: crimson; font-size: var(--size, 2rem);", "font-size"],
  ["--size is set to initial", "--size: initial; font-size: var(--size, 2rem);", "font-size"],
  ["--size is empty", "--size: ; font-size: var(--size, 2rem);", "font-size"],
  ["a fallback inside a fallback", "font-size: var(--size, var(--base-size, 1.25rem));", "font-size"],
  ["the fallback has a comma in it", "font-family: var(--font, Georgia, serif);", "font-family"],
  ["a number written next to px", "--gap: 20; margin-top: var(--gap)px;", "margin-top"],
  ["the number multiplied by 1px", "--gap: 20; margin-top: calc(var(--gap) * 1px);", "margin-top"],
  ["two properties that need each other", "--a: var(--b); --b: var(--a); color: var(--a, green);", "color"],
];

cases.forEach(([title, declarations, property], i) => {
  const page = createPage({ html: "<html><body><p class=\"t\">Text</p></body></html>", sheets: [{ origin: "author", css: `.t { ${declarations} }` }] });
  console.log(`${i + 1}. ${title}`);
  console.log(`   .t { ${declarations} }`);
  console.log(`   ${property}: ${page.computed(page.query(".t"), property)}`);
});

Output

1. --size is never declared
   .t { font-size: var(--size, 2rem); }
   font-size: 32px
2. --size holds a colour
   .t { --size: crimson; font-size: var(--size, 2rem); }
   font-size: 16px
3. --size is set to initial
   .t { --size: initial; font-size: var(--size, 2rem); }
   font-size: 32px
4. --size is empty
   .t { --size: ; font-size: var(--size, 2rem); }
   font-size: 16px
5. a fallback inside a fallback
   .t { font-size: var(--size, var(--base-size, 1.25rem)); }
   font-size: 20px
6. the fallback has a comma in it
   .t { font-family: var(--font, Georgia, serif); }
   font-family: Georgia, serif
7. a number written next to px
   .t { --gap: 20; margin-top: var(--gap)px; }
   margin-top: 0px
8. the number multiplied by 1px
   .t { --gap: 20; margin-top: calc(var(--gap) * 1px); }
   margin-top: 20px
9. two properties that need each other
   .t { --a: var(--b); --b: var(--a); color: var(--a, green); }
   color: green

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node fallbacks.mjs

  • Cases 1 and 3 use the fallback: --size has no value, and initial resets a custom property to having none.
  • Case 2 does not. crimson is a value, so font-size: crimson is tried and fails, and font-size inherits the parent’s 16px instead of using 2rem.
  • Case 4 is the subtle one: --size: ; is an empty value, not a missing one, so the fallback is skipped and the empty font-size fails the same way.
  • Case 7 shows that a var() cannot be glued to a unit. The value goes in token by token, so var(--gap)px gives the number 20 followed by the word px, not the length 20px, and the margin falls back to its initial 0px. Multiplying by 1px, as in case 8, adds the unit.
  • Case 9 is a cycle: --a and --b each need the other, so neither has a value, and the fallback is used.

The lesson on inheritance and computed values showed the same rule for color. Registering a custom property with a type, the subject of the next lesson, moves the failure from the property that uses the value to the custom property itself, which then inherits or takes a declared initial value.

Values stay tokens until they are used

An unregistered custom property holds tokens, and any calculation happens only where it is substituted. --pad: 1em on :root is not 16px. A heading with a 32px font that uses padding: var(--pad) gets 32px, and small print at 10px gets 10px. That is useful for spacing that should grow with the text, and confusing when you expected one fixed size.

For the same reason the browser cannot animate an unregistered custom property smoothly: it does not know what kind of value the tokens are. Animated with the Web Animations API from 0deg to 360deg over one second, an unregistered --angle reads 0deg until 499 ms and 360deg from 500 ms in all three engines, a single jump at the midpoint. A CSS transition on it changes nothing at all: the new value applies at once. Typed custom properties fix both.

Reading and writing them from JavaScript

Custom properties from the DevTools console JavaScript · console.js
// Paste into the DevTools console of a page whose style sheet sets :root { --brand: #0f766e; }.
const root = document.documentElement;

getComputedStyle(root).getPropertyValue("--brand");   // "#0f766e", kept as the style sheet wrote it
getComputedStyle(root).getPropertyValue("--missing"); // "": never declared

root.style.setProperty("--brand", "#6d28d9");         // an inline declaration on <html>: every var(--brand) follows
root.style.getPropertyValue("--brand");               // "#6d28d9"
root.style["--brand"] = "red";                        // no effect: "--brand" is not a property of the style object
root.style.removeProperty("--brand");                 // back to the style sheet's value

getComputedStyle(element).getPropertyValue("--name") returns the custom property’s value on that element as a string, as it was written, or an empty string when there is no value. element.style.setProperty("--name", value) adds an inline declaration to the element, and every var(--name) inside the element reads it; removeProperty() takes it away again. The usual property syntax does not work: element.style["--brand"] = "red" only adds an ordinary JavaScript property to the object, and the page does not change.

Because one write updates every use, a single setProperty() call can switch a theme or pass the pointer’s position to CSS, while the style sheet decides what each value does. Set the property on the smallest element that needs it, so the change stays inside that part of the page.

Custom properties or Sass variables?

Sass variables, such as $brand, look similar but work at a different time. Sass replaces them when it compiles, so the CSS that reaches the browser contains only the values. A custom property stays in the CSS and is worked out for each element while the page runs.

Sass variables or custom properties?
Criterion Sass variable ($brand)Custom property (--brand)
Resolved When Sass compiles the fileFor each element, in the browser
Different values in different parts of a page NoYes, through the cascade and inheritance
Changed by a media query or JavaScript NoYes
Usable in selectors and media queries YesNo, only in property values
In the CSS the browser receives No, compiled awayYes
When to choose Build-time constants, and loops that generate CSSThemes, component knobs and values that change at run time

Many projects use both: Sass for breakpoints, which a media query cannot read from a custom property, and custom properties for colours, spacing and everything a theme or a component variant changes.

Exercise: one colour, one place

A cooking site’s recipes.css writes its teal, its dark green and its white out in full wherever they are used, eleven colour values in seven rules. A festive card, .recipe--festive, should use the festival red #b91c1c for its border, its cooking time and its buttons.

The style sheet to refactor, and the page

exercise/recipes.css

/* recipes.css: the recipe cards of a cooking site. Every colour is written out where it is used. */
.site-header { background-color: #0f766e; color: #ffffff; padding: 16px; }
.recipe { border: 2px solid #0f766e; padding: 16px; margin: 0 0 24px; }
.recipe-title { color: #134e4a; margin: 0 0 8px; }
.recipe-time { color: #0f766e; font-weight: 700; }
.recipe-button { background-color: #0f766e; color: #ffffff; padding: 8px 16px; border-radius: 6px; }
.recipe-button--outline { background-color: #ffffff; color: #0f766e; }
.site-footer { color: #134e4a; border-top: 1px solid #0f766e; padding: 24px 16px; }

exercise/page.html

<html>
  <body>
    <header class="site-header">Weeknight Kitchen</header>
    <main>
      <article class="recipe">
        <h2 class="recipe-title">Lemon rice</h2>
        <p class="recipe-time">25 min</p>
        <button class="recipe-button">Start cooking</button>
        <button class="recipe-button recipe-button--outline">Save</button>
      </article>
      <article class="recipe recipe--festive">
        <h2 class="recipe-title">Saffron kheer</h2>
        <p class="recipe-time">50 min</p>
        <button class="recipe-button">Start cooking</button>
        <button class="recipe-button recipe-button--outline">Save</button>
      </article>
    </main>
    <footer class="site-footer">New recipes every Friday.</footer>
  </body>
</html>

Change only recipes.css. Give each colour a custom property declared once, use var() everywhere else, and add a .recipe--festive rule that sets one or two custom properties and nothing else. Run node check.mjs in the exercise folder after each change. As given, six of its eighteen tests fail:

The sample tests, run on recipes.css as given JavaScript · exercise/check.mjs
// Sample tests for recipes.css. Change recipes.css, then run `node check.mjs` in this folder. The page and the style
// sheet are read by the cascade model in ../css-model.
import { readFileSync } from "node:fs";
import { createPage } from "../css-model/index.mjs";
import { parseStyleSheet } from "../css-model/stylesheet.mjs";

const css = readFileSync("recipes.css", "utf8");
const page = createPage({ html: readFileSync("page.html", "utf8"), sheets: [{ origin: "author", css }] });

const plain = ".recipe:not(.recipe--festive)";
const festive = ".recipe--festive";
const expectations = [
  ["the header's background is #0f766e", ".site-header", "background-color", "#0f766e"],
  ["the header's text is #ffffff", ".site-header", "color", "#ffffff"],
  ["a card's border is #0f766e", plain, "border-top-color", "#0f766e"],
  ["a card's title is #134e4a", `${plain} .recipe-title`, "color", "#134e4a"],
  ["a card's time is #0f766e", `${plain} .recipe-time`, "color", "#0f766e"],
  ["a card's button is #0f766e", `${plain} .recipe-button:not(.recipe-button--outline)`, "background-color", "#0f766e"],
  ["a card's button text is #ffffff", `${plain} .recipe-button:not(.recipe-button--outline)`, "color", "#ffffff"],
  ["an outline button's background is #ffffff", `${plain} .recipe-button--outline`, "background-color", "#ffffff"],
  ["an outline button's text is #0f766e", `${plain} .recipe-button--outline`, "color", "#0f766e"],
  ["the footer's text is #134e4a", ".site-footer", "color", "#134e4a"],
  ["the footer's border is #0f766e", ".site-footer", "border-top-color", "#0f766e"],
  ["the festive card's border is #b91c1c", festive, "border-top-color", "#b91c1c"],
  ["the festive card's time is #b91c1c", `${festive} .recipe-time`, "color", "#b91c1c"],
  ["the festive card's button is #b91c1c", `${festive} .recipe-button:not(.recipe-button--outline)`, "background-color", "#b91c1c"],
  ["the festive card's outline button text is #b91c1c", `${festive} .recipe-button--outline`, "color", "#b91c1c"],
  ["the festive card's title stays #134e4a", `${festive} .recipe-title`, "color", "#134e4a"],
];

let failed = 0;
const result = (ok, name, detail = "") => {
  if (!ok) failed++;
  console.log(`${ok ? "pass" : "FAIL"}  ${name}${detail}`);
};
for (const [name, selector, property, want] of expectations) {
  const el = page.query(selector);
  const got = page.computed(el, property);
  if (got === want) {
    result(true, name);
    continue;
  }
  const { winner } = page.explain(el, property);
  result(false, name, `: got ${got}${winner ? ` from ${winner.source}` : ", inherited or initial"}`);
}

// Each colour value is written once (named colours, hex, rgb() and the like), so a palette change is one edit.
const counts = new Map();
for (const m of css.replace(/\/\*[\s\S]*?\*\//g, " ").matchAll(/#[0-9a-f]{3,8}\b|\b(?:rgba?|hsla?|oklch|oklab)\([^)]*\)/gi)) {
  const colour = m[0].toLowerCase();
  counts.set(colour, (counts.get(colour) ?? 0) + 1);
}
const repeated = [...counts].filter(([, n]) => n > 1).map(([c, n]) => `${c} ${n} times`);
result(repeated.length === 0, "each colour value is written once", repeated.length ? `: ${repeated.join(", ")}` : "");

// The festive variant only turns knobs: one or two custom properties, nothing else.
const own = parseStyleSheet(css).filter((d) => d.selectorText === ".recipe--festive");
const others = own.filter((d) => !d.property.startsWith("--"));
const ok = own.length > 0 && own.length <= 2 && others.length === 0;
result(ok, "the .recipe--festive rule sets one or two custom properties and nothing else", own.length === 0 ? ": there is no .recipe--festive rule" : ok ? "" : `: it sets ${own.map((d) => d.shorthand ?? d.property).join(", ")}`);

const total = expectations.length + 2;
console.log(`${total - failed} of ${total} tests pass`);
process.exitCode = failed ? 1 : 0;

Output (exit status 1)

pass  the header's background is #0f766e
pass  the header's text is #ffffff
pass  a card's border is #0f766e
pass  a card's title is #134e4a
pass  a card's time is #0f766e
pass  a card's button is #0f766e
pass  a card's button text is #ffffff
pass  an outline button's background is #ffffff
pass  an outline button's text is #0f766e
pass  the footer's text is #134e4a
pass  the footer's border is #0f766e
FAIL  the festive card's border is #b91c1c: got #0f766e from .recipe { border: 2px solid #0f766e }
FAIL  the festive card's time is #b91c1c: got #0f766e from .recipe-time { color: #0f766e }
FAIL  the festive card's button is #b91c1c: got #0f766e from .recipe-button { background-color: #0f766e }
FAIL  the festive card's outline button text is #b91c1c: got #0f766e from .recipe-button--outline { color: #0f766e }
pass  the festive card's title stays #134e4a
FAIL  each colour value is written once: #0f766e 6 times, #ffffff 3 times, #134e4a 2 times
FAIL  the .recipe--festive rule sets one or two custom properties and nothing else: there is no .recipe--festive rule
12 of 18 tests pass

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node check.mjs

First hint: name each colour by its job, such as --brand for the teal, and let .recipe--festive change that job’s value. If you also add a token made from another one, such as --accent: var(--brand) on :root, where is its var() replaced?

CSS Toolkit The Variables tab finds the colours a style sheet repeats and turns them into custom properties with var().

Self-check

Check yourself

6 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.

  1. Question 1 of 6 A style sheet has :root { --space: 12; } and .card { margin-top: var(--space)px; }. What is the card's margin-top?

    Choose one answer.

    Show the answer to question 1

    Answer: 0px: the value is the number 12 followed by the word px, which is not a length

    var() inserts tokens, and tokens never join: 12 and px stay a number and a word. The declaration is invalid at computed-value time, so margin-top takes its initial value, 0px. Write calc(var(--space) * 1px) instead.

  2. Question 2 of 6 Inside a body with a 16px font, a paragraph has --size: crimson; font-size: var(--size, 1.25rem);. What is its font size?

    Choose one answer.

    Show the answer to question 2

    Answer: 16px, inherited from the body

    The fallback is used only when --size has no value. crimson is a value, so font-size: crimson is tried, fails at computed-value time, and font-size behaves as unset. font-size is inherited, so the paragraph takes the body's 16px.

  3. Question 3 of 6 :root { --brand: teal; --focus: var(--brand); } and .dark { --brand: white; }. A button inside .dark has outline-color: var(--focus). What colour is its outline?

    Choose one answer.

    Show the answer to question 3

    Answer: Teal: --focus was worked out on :root and inherited as teal

    A var() inside a custom property is replaced on the element that declares it. --focus is declared on :root, where --brand is teal, so the button inherits teal. Declaring --focus again on .dark, or on the button itself, would make it follow.

  4. Question 4 of 6 Where can var(--x) be used?

    Choose every answer that is right.

    Show the answer to question 4

    Answer:

    • As part of a value, such as border: 1px solid var(--x)
    • As a whole property value, such as color: var(--x)

    The specification allows var() only in property values, whole or in part. A media query condition, a property name and a selector are not property values: the media query never matches and the declaration with a var() name is dropped.

  5. Question 5 of 6 Which declaration of --x makes color: var(--x, navy) use the fallback?

    Choose one answer.

    Show the answer to question 5

    Answer: --x: initial;

    initial resets a custom property to having no value, so the fallback is used. --x: ; is an empty value, which is a value: color becomes empty, fails, and inherits instead. red is simply used.

  6. Question 6 of 6 Which line changes the colour of everything that uses var(--brand) inside el?

    Choose one answer.

    Show the answer to question 6

    Answer: el.style.setProperty("--brand", "#6d28d9")

    setProperty() adds an inline declaration of the custom property, which every var(--brand) inside el then reads. Assigning el.style["--brand"] only creates an ordinary JavaScript property on the object, and el.style.brand names no CSS property at all.

Interview questions

When does the fallback in var() apply? Only when the custom property has no value: it was never declared on the element or an ancestor, it was set to initial, or it is part of a dependency cycle. A value of the wrong type does not trigger it. With --size: crimson, font-size: var(--size, 2rem) substitutes crimson, the declaration fails at computed-value time, and font-size behaves as unset, so it inherits the parent’s size. An empty value, --size: ;, also counts as a value. Everything after the first comma is the fallback, and it may contain another var().

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Mid to senior

    A design system declares --brand and --link: var(--brand) on :root. A campaign page sets --brand on its .campaign wrapper, but the links inside keep the old colour. Explain why, fix it, and say how you would stop the next derived token from breaking the same way.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Fresher to mid

    After a refactor, spacing tokens are bare numbers, such as --space-2: 8, and components write padding: var(--space-2)px. Every component lost its padding, yet DevTools shows no crossed-out declaration. What happened, and how should spacing tokens be stored?

    Model answer, Pro. First step and follow-ups

Key takeaways

  • A custom property is a property named --something; var(--something) puts its value into another property’s value, and nowhere else.
  • Custom properties cascade and inherit: a value on :root reaches the whole page, and a value on a component overrides it inside that component.
  • A var() inside a custom property is replaced on the element that declares it, and children inherit the result. Declare derived values on the components that use them.
  • The fallback is used only when there is no value. A wrong value makes the property behave as unset, and var(--n)px is a number and a word, not a length: write calc(var(--n) * 1px).
  • Unregistered custom properties hold tokens: 1em is resolved where it is used, and animations jump at the midpoint.
  • JavaScript reads them with getComputedStyle(el).getPropertyValue("--x") and writes them with el.style.setProperty("--x", value).

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.