Declare and inherit
A custom property is a property whose name starts with two dashes, like --brand. This :root rule stores two design choices: the colour tomato and an 8px gap. On its own it styles nothing; it just holds a value.
The selector :root matches the top element of the page, html. So the browser stores --brand: tomato and --gap: 8px on the html element, just as it stores any other property it works out for that element.
Custom properties are inherited: unless an element sets its own value, it takes its parent's. So body, the card and both buttons now hold tomato and 8px too, passed down the tree from html.
Read with var()
The button rule says background: var(--brand). For each button, the browser replaces var(--brand) with that button's own --brand value, tomato, and var(--gap) with 8px. It does this while it works out each element's style.
Now add .card { --brand: teal; }. The card element gets its own --brand, and its child, the Save button, inherits teal from the card. The Buy button is outside the card, so it still inherits tomato from html.
The button rule did not change, yet Buy is tomato and Save is teal. var() is worked out again for every element, from the value that element holds. Setting a variable on one part of the page is how themes work.
color: var(--ink, white) asks for --ink, which no rule declares. An undeclared custom property holds the guaranteed-invalid value, so var() uses the fallback after the comma: white. With no fallback, color would act as unset.
Tokens at scale
Change --gap in :root from 8px to 16px. The new value flows down to every element and both buttons double their padding. One edit restyles every rule that reads the token, which is why design systems keep their choices in custom properties.
What happens
;; step 1/8 · declare
style.css, lines 1 to 4
:root {
--brand: tomato;
--gap: 8px;
}
--brand a custom property, value tomato
--gap a custom property, value 8px
names start with --, and are case-sensitive:
--brand and --Brand are two different properties
A real browser reads the whole stylesheet at once and works out every element's style in one pass. The figure adds the rules and declarations one at a time so you can see what each one does. Before the button rule applies, a button keeps the browser's own default look (a light grey face and dark text). The padding drawn on each button is to scale with the --gap value, not with real screen pixels.