skip to content

Custom Properties

Custom properties are real cascading, inheriting values resolved at computed-value time — not preprocessor variables substituted at build time. Interviewers ask how they differ from Sass variables and what happens when a var() fallback chain fails.

part ofCSSoverview, primer and where to startread it →
on this pageshow

questions

5

In CSS, how do you declare a custom property and use it in a value with var(), and what does the second argument in var(--name, fallback) do?

level: juniorimportance: must knowfreq 72%

answer

  1. two dashes make it a property
  2. cascades and inherits like color
  3. comma introduces the fallback
  4. everything after first comma counts
  5. covers missing, not wrong

basics

~20 s

Declare a custom property as a normal declaration whose name starts with two dashes, such as --gap: 12px, and read it back with var(--gap). The optional second argument, var(--gap, 8px), applies only when --gap is not defined.

solid answer

~50 s

A custom property is any declaration whose name begins with two dashes: `.card { --gap: 12px; }`. It is a real CSS property, so it takes part in the cascade and it inherits by default, which means every descendant of `.card` sees `--gap`. You consume it with `var(--gap)` anywhere a value is allowed, including inside `calc()` and inside shorthands: `padding: var(--gap)`. Names are case-sensitive, so `--Gap` and `--gap` are two different properties. `var()` accepts an optional fallback after the first comma — `var(--gap, 8px)` resolves to `8px` when `--gap` has not been set on that element — and everything after that first comma counts as the fallback, so `var(--font, Georgia, serif)` is legal. The fallback covers the *missing* case only, not the case where the property exists but holds something the target property cannot use.

code

css · 18 lines
css
:root {
  --gap: 12px;
  --accent: #1a73e8;
}

.card {
  padding: var(--gap);
  border: 1px solid var(--accent);
}

.card--tight {
  --gap: 4px;
}

.sidebar {
  /* --gap is not set here, so the fallback applies */
  margin: var(--sidebar-gap, 16px);
}

go deeper

for a junior

Be ready to write the syntax from memory: --gap: 12px to declare, var(--gap) to use, var(--gap, 8px) for a fallback when the property is missing. Say plainly that the name needs both dashes.

for a middle

Explain that a custom property is an ordinary inherited property that cascades, so where you declare it decides which elements see it, and that everything after the first comma in var() is the fallback.

for a senior

Show the failure modes you have debugged: a fallback that never fires because the property is defined but wrong, a bare number that will not concatenate with a unit, and a case-mismatched name that silently resolves to nothing.

for a principal

Own the convention: where defaults live, whether values carry units, and how deep var() chains are allowed to nest before a stylesheet becomes untraceable for the next reader.

## What a custom property actually is A custom property is a CSS declaration whose property name starts with two hyphens, for example `--gap: 12px`. The standard properties are a fixed list defined by the specification; custom property names are yours to invent. The browser does not know or care what `--gap` means — it stores the declared value as a stream of tokens and hands that stream back wherever you ask for it. The important consequence is that this is a *property*, not a special build-time construct. It lives inside a rule with a selector, it competes in the cascade like any other declaration (later wins over earlier at equal specificity, higher specificity wins, `!important` wins), and it **inherits by default**, flowing down to descendants the way `color` does. ## Declaring one ```css :root { --gap: 12px; --brand: #1a73e8; } .card { --gap: 8px; /* applies to .card and everything inside it */ } ``` `:root` matches the document's root element, which is where people usually park global defaults. Nothing forces that: you can declare a custom property on any selector, and even on a single element through its `style` attribute. Two details bite beginners. First, names are **case-sensitive** — `--Brand` and `--brand` are unrelated properties, unlike `COLOR` and `color`. Second, the declared value is almost unrestricted: `--x: 1px solid;` and even `--x: hello world;` parse fine, because validity is not checked until something tries to use the value. ## Reading it back with var() ```css .card { padding: var(--gap); border: 1px solid var(--brand); width: calc(100% - var(--gap) * 2); } ``` `var()` can appear anywhere a value, or part of a value, is allowed. It can sit inside `calc()`, inside a shorthand, and inside the value of another custom property, which is how layered token sets are built: ```css :root { --space: 4px; --gap-lg: calc(var(--space) * 4); } ``` Three things `var()` cannot do, and interviewers like all three: - It cannot build a **property name**. `var(--prop): red` is not valid CSS. - It cannot appear in a **selector**. - It cannot appear in a **media query condition**; `@media (min-width: var(--bp))` does not work in browsers today. ## The fallback argument `var()` takes an optional second argument after the first comma: ```css .card { padding: var(--gap, 8px); } ``` If `--gap` is not defined on the element being styled, the substitution uses `8px`. "Not defined" is precise: the property has never been set, or was explicitly reset, so it holds the guaranteed-invalid value. Everything after the *first* comma is the fallback, commas included, which is why font stacks work: ```css font-family: var(--font, Georgia, "Times New Roman", serif); ``` Fallbacks nest, so you can chain preferences: `var(--a, var(--b, 4px))` tries `--a`, then `--b`, then `4px`. ## What the fallback does not do The fallback only covers the missing case. If `--gap` **is** defined but holds something the target property cannot accept — say `--gap: red` used in `padding: var(--gap, 8px)` — you do not get `8px`. The substitution happens, the result is nonsense for `padding`, and the declaration becomes invalid at computed-value time, which is a different and more surprising failure mode than a missing property. ## Tokens are spliced, not glued A classic trap: ```css :root { --size: 10; } .box { width: var(--size)px; } /* does not work */ .box { width: calc(var(--size) * 1px); } /* works */ ``` Substitution inserts the stored tokens next to the surrounding tokens; it does not concatenate text into a new dimension token, so `10` and `px` never become `10px`. Multiply by a unit inside `calc()` instead. This is also why storing unit-bearing values (`--size: 10px`) is usually simpler than storing bare numbers. ## The shape you will actually write Global defaults on `:root`, local overrides on the component or a variant class, and a single consuming rule that never has to change: ```css :root { --accent: #1a73e8; } .button { background: var(--accent); } .button--danger { --accent: #c5221f; } ``` That one-declaration-many-values shape is the whole point of custom properties, and it is only possible because they cascade and inherit like real properties.

  • If --size is 10, why does width: var(--size)px not produce a 10px width?
    Because substitution splices token streams rather than concatenating text: the `10` and the `px` never fuse into a single dimension token, so the value is not a valid length and the declaration is dropped at computed-value time. Multiply by a unit instead: `width: calc(var(--size) * 1px)`. Storing the unit in the property, `--size: 10px`, avoids the problem entirely.
  • Can you use var() to choose which property you are setting, like var(--prop): red?
    No. `var()` substitutes into values only, never into property names, selectors, or at-rule preludes. A custom property's value is a token stream the browser splices into a value position; property names are parsed before any substitution happens. If you need to switch which property changes, switch the rule or the class, not the property name.
  • Are custom property names case-sensitive?
    Yes, unlike standard property names. `--Brand` and `--brand` are two independent properties, and `var(--Brand)` will not find a value declared as `--brand`. This is a common source of silent failures because the result is not an error — the property is simply missing, so you get the `var()` fallback if you supplied one, and invalid-at-computed-value-time behaviour if you did not.

saying these in an interview costs you the question

  • Thinks the var() fallback also rescues invalid defined values
  • Writes var(gap) or var(-gap) instead of var(--gap)
  • Assumes --Gap and --gap are the same property
  • Expects var(--n)px to concatenate into a length
  • Believes var() works inside a media query condition

context

open as a page

How do CSS custom properties differ from Sass variables, and what can you do with one that you cannot do with the other?

level: middleimportance: must knowfreq 66%

basics

~20 s

CSS custom properties are real values in the cascade: they inherit, can be redefined per selector or per element, and the browser resolves them at computed-value time. Sass variables are compile-time text that never reaches the browser.

open as a page

In CSS, a single rule .card { border-color: var(--accent); } renders a different border color on each card. How can one declaration produce different values, and on which element is var(--accent) resolved?

level: middleimportance: should knowfreq 52%

basics

~20 s

Custom properties are inherited properties resolved separately for every element. var(--accent) is substituted with whatever --accent computes to on the element being styled, so each card that inherits or declares a different --accent gets a different border color from the same rule.

open as a page

A CSS rule contains color: black; color: var(--brand); and --brand holds the value 12px. The text renders in the color inherited from its parent, not black. Why does the earlier declaration not win?

level: seniorimportance: should knowfreq 42%

basics

~20 s

var() is checked when values compute, not when CSS parses. The var() declaration looks valid at parse time, so it wins the cascade and discards color: black; substituting 12px then makes it invalid at computed-value time, so color falls back to the inherited value.

open as a page

What does registering a custom property with the CSS @property at-rule give you that a plain --name: declaration does not?

level: seniorimportance: nice to knowfreq 30%

basics

~20 s

@property registers a custom property with a syntax type, an inherits flag and an initial-value. That buys type checking, a real fallback instead of an unset value, the ability to opt out of inheritance, and values the browser can interpolate.

open as a page