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?
answer
- parse time versus computed-value time
- var() declarations are assumed valid
- earlier declaration is already discarded
- computes as if unset
- inherited value or initial value
basics
~20 svar() 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.
solid answer
~50 sThis is the invalid-at-computed-value-time rule. When the parser reads `color: var(--brand)` it cannot know what `--brand` holds, so any declaration containing `var()` is treated as syntactically valid. It therefore competes normally in the cascade, wins as the later declaration, and `color: black` is discarded right there — the usual safety net of "a bad value is ignored and the previous one applies" does not fire, because the bad value was not visible at parse time. Substitution happens later, at computed-value time. `12px` is not a `<color>`, so the declaration becomes invalid at computed-value time, and the specification says the property then computes as if `unset`: the inherited value for an inherited property like `color`, the initial value for a non-inherited one. Note that `var(--brand, red)` would not rescue it either, since `--brand` is defined — the fallback only covers a missing property.
code
css · 13 lines:root {
--brand: 12px;
}
p {
color: black;
color: var(--brand);
}
.box {
padding: 8px;
padding: var(--brand);
}go deeper
Know the vocabulary: a value that only turns out to be wrong after var() substitution is invalid at computed-value time, and the property then behaves as if you had written unset.
Explain why the earlier declaration is gone: any declaration containing var() is assumed valid at parse time, so it wins the cascade before the bad value is ever seen, and validation happens later during computation.
Demonstrate the debugging path — computed value matches neither declaration, resolve the custom property on the element being styled, check inheritance from ancestors — and name the structural fix rather than patching one rule.
Own the prevention: typed registration for tokens that cross team boundaries, a convention about units in token values, and a rule against reusing one token across incompatible value grammars.
## Two different moments where CSS can reject a value CSS normally protects you with parse-time validation. In `color: black; color: qwerty;` the second declaration is garbage the parser recognises as garbage, so it is thrown away immediately and never enters the cascade; `black` applies. Authors lean on this constantly — it is the basis of the old "declare a fallback first, then the fancy value" pattern. `var()` breaks that model, deliberately. At parse time the browser has no idea what `--brand` contains: custom property values are stored as token streams and are not validated against any grammar. So the specification says a declaration containing `var()` is **assumed valid at parse time**. It enters the cascade like anything else, and here it wins — same selector, same specificity, later in source order. `color: black` is discarded at that moment and is not recoverable. The real check happens later, when the browser computes the element's styles and performs substitution. Now it has `color: 12px`, which is not a `<color>`. The declaration is **invalid at computed-value time** (often abbreviated IACVT). ## What IACVT actually does The rule is precise and worth memorising: the property computes to the same thing it would if you had written `unset`. - For an **inherited** property such as `color`, `font-size` or `visibility`, that means the **inherited value** from the parent. - For a **non-inherited** property such as `padding`, `background-color` or `border-width`, that means the property's **initial value** — `0`, `transparent`, `medium` and so on. That is why the symptom in the question is so confusing: the text takes the parent's colour, a value that appears nowhere in the rule you are staring at. If the same mistake had hit `padding`, you would have got `0` instead, not the earlier `padding` declaration. ```css :root { --brand: 12px; } /* a length, by mistake */ p { color: black; color: var(--brand); /* wins the cascade, then becomes IACVT */ } /* computed color = inherited color, not black */ .box { padding: 8px; padding: var(--brand-pad, 4px); /* if --brand-pad holds "red": IACVT -> 0 */ } ``` ## Why the var() fallback does not save you The fallback in `var(--brand, red)` fires on exactly one condition: the custom property is **missing** on that element — never declared, or reset to the guaranteed-invalid value. If `--brand` is defined and simply holds the wrong kind of value, substitution goes ahead with the wrong value, and IACVT follows. The fallback protects against absence, not against wrongness. This trips up people who add fallbacks everywhere expecting them to act as type guards. ## Ways the wrong value gets in In a real codebase the usual causes are mundane: - A unitless number stored where a length is expected: `--gap: 10`, consumed as `padding: var(--gap)`. - A token that carries a unit and gets used where a number is expected, or vice versa. - A property intended for one type reused for another — a spacing token pushed into `color`. - An empty custom property: `--x: ;` is a valid declaration holding an empty value, which substitutes to nothing and usually leaves the declaration invalid. - A value set from outside the stylesheet that arrives as a string not matching the target property's grammar. ## How to make the failure safe The strongest fix is to give the property a type, by registering it with `@property`. A registered property has a declared `syntax` and an `initial-value`, so a value that does not match is rejected and the property uses its initial value (or the inherited value, when it inherits) rather than collapsing the consuming declaration to `unset`: ```css @property --brand { syntax: "<color>"; inherits: true; initial-value: black; } ``` Now assigning `12px` to `--brand` is caught at the source: consumers keep seeing a real colour. Without registration you are relying on discipline — consistent units in tokens, one property per value type, and never reusing a token across incompatible grammars. ## Debugging it The tell is that the computed value in devtools matches neither declaration in the rule. Inspect the element's computed styles, then look up what the custom property actually resolves to on **that** element rather than on the element where you think you declared it — inheritance means the value may come from an ancestor you were not looking at. Once you see the resolved token stream, the mismatch is usually obvious. And remember that reordering declarations will not help: the `var()` declaration wins the cascade whatever order you put it in, as long as it is not outranked.
- Why does the old fallback pattern of declaring a safe value before a risky one stop working with var()?Because that pattern relies on parse-time rejection: the parser drops the unrecognised value and the earlier declaration survives. A declaration containing `var()` is assumed valid at parse time, so it wins the cascade and discards the safe one immediately. The failure surfaces later, at computed-value time, when there is nothing left to fall back to.
- How does the outcome differ between an inherited and a non-inherited property?Invalid at computed-value time makes the property compute as if you had written `unset`. For an inherited property such as `color` or `font-size` that is the parent's computed value; for a non-inherited property such as `padding` or `background-color` it is the property's initial value — `0`, `transparent`, and so on. Same rule, two very different-looking symptoms.
- Would registering the custom property with @property have prevented this?Yes, at the source. A property registered with `syntax: "<color>"` and an `initial-value` rejects a value that does not match the declared type, so consumers see the initial value (or the inherited one) instead of a length. The consuming declaration stays valid, which turns a mystery inherited colour into a predictable fallback.
saying these in an interview costs you the question
- Expects the earlier valid declaration to apply as a fallback
- Thinks var(--x, fallback) rescues a defined-but-wrong value
- Says the browser ignores the whole rule
- Believes reordering the declarations fixes it
- Assumes an invalid value leaves the property at its previous value