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?
answer
- resolved per element, not per rule
- inherits like color does
- nearest declaring ancestor wins
- two separate cascade contests
- nested var() resolves at the consumer
basics
~20 sCustom 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.
solid answer
~50 sSubstitution is per element, not per rule. When the browser computes styles for a card, it first resolves `--accent` **on that card** — using the card's own declaration if it has one, otherwise the value inherited from its nearest ancestor that declared it — and then substitutes that into `border-color`. The consuming rule is written once and never changes; what varies is where `--accent` is declared. So a variant class `.card--danger { --accent: crimson; }`, a declaration on a wrapper that the card inherits, or an inline `style="--accent: teal"` on one element all steer the same declaration to different results. Two cascades are running independently here: one decides which `--accent` declaration wins for the element, the other decides which `border-color` declaration wins. That separation is the whole scoping model — override the value near the element, keep the consumer generic.
code
css · 16 lines:root {
--accent: #888888;
--border: 2px solid var(--accent);
}
.card {
border: var(--border);
}
.card--danger {
--accent: crimson;
}
.panel .card {
--accent: teal;
}go deeper
Know that custom properties inherit, so declaring one on a parent or a variant class changes what var() gives you on the elements underneath, without editing the rule that uses it.
Explain resolution order: the browser resolves the custom property on the element being styled — own declaration first, otherwise inherited — and only then substitutes it into the consuming declaration.
Show how you use scope deliberately: overrides declared on the nearest sensible ancestor, per-item values set inline, and consuming rules that never need to know which variant they are in.
Own where properties are allowed to be declared and how far they may travel, so that a value changed on one wrapper has a predictable blast radius rather than silently retheming half a page.
## One declaration, many results ```css .card { border-color: var(--accent); } :root { --accent: #888; } .card--danger{ --accent: crimson; } .panel { --accent: teal; } ``` Every card matches the same `border-color` declaration. What differs is the value `--accent` computes to *on that specific card*, and the browser resolves that independently for each element it styles. ## Where the value comes from Custom properties are inherited by default, so the lookup for `--accent` on a given card works exactly like the lookup for `color`: 1. Does any rule declare `--accent` on this element? If several do, the cascade picks a winner by origin, importance, specificity and order. 2. If none does, the element inherits the computed value of `--accent` from its parent — which may itself have inherited it, all the way up to the root. 3. If nothing in the chain declared it, the property is missing, and `var(--accent)` falls back to its second argument if you supplied one. A card inside `.panel` therefore sees `teal`, a card carrying `.card--danger` sees `crimson`, and a card sitting anywhere else sees the root's `#888` — from one consuming rule. ## Two independent cascades This is the part people get wrong. The contest over `--accent` and the contest over `border-color` are separate. ```css .card { border-color: var(--accent); } /* the consumer */ .sidebar .card { --accent: navy; } /* wins the --accent contest */ ``` `.sidebar .card` never mentions `border-color`, yet it changes the border. It wins the cascade *for the custom property*, and the unchanged `.card` rule then substitutes the new value. Specificity applies to each contest on its own terms: a higher-specificity `border-color` declaration elsewhere could still beat the consuming rule entirely, and that has nothing to do with which `--accent` won. ## Scoping is by subtree, not by file Because resolution follows inheritance, a custom property declared on an element is visible to that element and everything under it, regardless of which stylesheet the consuming rule was written in: ```css .theme-invert { --accent: white; } /* every consumer inside .theme-invert now resolves --accent to white */ ``` That gives you a scoping tool with no build step and no naming scheme: put the declaration on the smallest ancestor that should be affected. Setting it on `:root` is just the widest case of the same mechanism. Setting it inline on one element — `<div class="card" style="--accent: gold">` — is the narrowest, and is the usual way a per-item value reaches CSS. ## Consequences worth knowing **Declaring a property costs nothing where it is not consumed.** `--accent` on `:root` does not draw anything; it only matters where some declaration reads it. That is why the pattern scales: many declared properties, few consuming rules. **A property can reference another, and the reference is also resolved per element.** ```css :root { --accent: teal; --border: 2px solid var(--accent); } .card { border: var(--border); } .card--danger { --accent: crimson; } ``` The card with `--accent: crimson` gets a crimson border, because `--border` is a token stream containing an unresolved `var()`, and that inner reference is resolved against the element where `--border` is finally used, not where it was declared. This is one of the most useful and least obvious behaviours of custom properties. **Cycles resolve to nothing.** If `--a: var(--b)` and `--b: var(--a)`, both properties are treated as invalid at computed-value time on that element rather than looping. **A pseudo-element inherits from its originating element.** `::before` sees the custom properties of the element it belongs to, so the same scoping logic covers generated content. ## The shape to describe in an interview Write the consumer once, close to the component it styles. Override the value as near the affected elements as possible — variant class, wrapper, or the element itself. Reach for the root only for genuine defaults. When someone asks "how do I change this in one place", the answer with custom properties is usually "declare the property on a different element", not "add another rule for the property that draws".
- If --border is declared on :root as 2px solid var(--accent), why does a card that redeclares --accent still get its own color?Because a custom property's value is stored as an unresolved token stream. The inner `var(--accent)` is not substituted where `--border` is declared; it is substituted when `--border` is finally used in a real property, and that resolution happens on the consuming element. So the card's own `--accent` wins.
- Does declaring lots of custom properties on :root that most elements never read cost anything?Each one is a real inherited declaration the browser stores and resolves for elements, so a very large set on a very large DOM is measurable, but declaring a property does not draw or lay out anything by itself. The practical advice is to scope properties to the subtree that consumes them rather than to fear root defaults.
- What happens if two custom properties reference each other?The cycle is detected and both are treated as invalid at computed-value time on that element — the browser does not loop or pick an arbitrary winner. Anything consuming them then behaves as if the property were missing with no valid substitution, so a consuming declaration computes as if unset. Devtools showing an unexpectedly empty value is the usual clue.
saying these in an interview costs you the question
- Thinks var() is resolved once per rule, not per element
- Believes custom properties only work when declared on :root
- Says a variant class must redeclare the drawing property too
- Assumes nested var() is resolved where the property is declared
- Confuses the custom property cascade with the consuming property's cascade