The Problem: One Colour, Forty-Seven Places
Suppose you are building a fan website for your favourite IPL team using HTML and CSS. Every page uses the team's signature colour — on the header background, on button borders, on link underlines, on the score ticker, on hover effects. By the time you finish four pages, that one hex code, say #f9cb08 for Chennai Super Kings' yellow, is typed out in forty-seven different places across your stylesheet.
Now the team announces a slightly refreshed shade for the new season. You open your CSS file and start replacing #f9cb08 with the new code, one occurrence at a time. You get to occurrence thirty-nine, get distracted, and stop. Your site now has thirty-eight elements in the new shade and nine still in the old one — a visual bug that is genuinely hard to spot by eye, because both colours are "yellow" at a glance. This is not a rare mistake. It is what happens, reliably, whenever a value is repeated by hand instead of named once and reused.
CSS custom properties exist to remove exactly this class of bug. They let you write a value once, give it a name, and refer to that name everywhere the value is needed. Change the one definition, and every place that used the name updates automatically. This chapter builds that idea from the ground up, shows you the exact syntax the browser understands, and — importantly — corrects a very common misunderstanding about what these "variables" actually are.
A Variable You Already Know: Algebra
You have used this idea before, just not in CSS. In Class 9 mathematics, if you write x = 5 and then evaluate the expression 2x + 3, you get 13. If tomorrow the problem changes and now x = 10, you do not go back and rewrite "2x + 3" as a new formula — you simply recompute using the new value of x, and the expression gives 23. The formula never changed; only the value bound to the name x changed, and every place that used x picked up the new value automatically.
A CSS custom property is the same mechanism applied to a stylesheet. You declare a name (starting with two dashes, as you will see below) and bind a value to it once. Every place in your CSS that refers to that name by using the var() function will use whatever value is currently bound to it — and if you change the one declaration, every one of those places updates the next time the browser repaints the page. You are not copy-pasting a colour code forty-seven times; you are writing a formula that says "wherever you see this name, substitute the current value," exactly like algebra.
Declaring and Using a Custom Property
A custom property is declared inside a normal CSS rule, just like any other property, but its name must begin with two hyphens. This double-hyphen prefix is not decoration — it is how the browser tells a custom property apart from a real, built-in CSS property such as color or padding. To actually use the value stored in a custom property, you do not write the name directly as a value. You pass it through the var() function.
:root {
--brand-color: #0b5fff;
--gap: 16px;
}
.card {
border: 2px solid var(--brand-color);
padding: var(--gap);
}
.card__title {
color: var(--brand-color);
}
Trace this exactly as the browser would. First, the browser reads the :root rule and stores two custom properties: --brand-color is bound to #0b5fff, and --gap is bound to 16px. Next, for any element matching .card, the declaration border: 2px solid var(--brand-color) is resolved by substituting the stored value, giving the computed rule border: 2px solid #0b5fff. The declaration padding: var(--gap) resolves to padding: 16px. Finally, for an element matching .card__title, color: var(--brand-color) resolves to color: #0b5fff. Notice that the same name, --brand-color, was substituted correctly in two completely different rules, for two different properties (border and color). A custom property can hold any valid CSS value — a colour, a length, a font name, even a whole list — because unlike a real property, the browser does not check its content against a fixed type until it is actually used inside var().
One rule students very commonly get wrong on first contact: --brand-color and --Brand-Color are two entirely different, unrelated custom properties. Standard CSS property names like color or background are case-insensitive, but custom property names are case-sensitive. Mixing up capitalisation between where you declare a variable and where you call it inside var() is a real, silent bug — the browser will not show an error; it will simply treat your call as referring to an property.
Where You Declare It Matters: :root and the Cascade
:root is a CSS selector that matches the single topmost element of every HTML document — effectively the <html> element itself. Declaring a custom property inside :root puts it at the very top of the document tree, which is why it behaves like a "global" setting: every single element on the page sits somewhere underneath :root in the tree, and CSS custom properties are, by default, inherited — their value flows down from a parent element to its children, and their children, unless something along the way redefines it.
This is a genuinely important structural fact, not a minor detail: most ordinary CSS properties (like border or padding) do not inherit — a border on a parent element does not automatically appear on its children. Custom properties are unusual in that they inherit by default. That is precisely what makes declaring them at :root useful: the value is available to every descendant in the tree without your having to repeat the declaration anywhere else.
The diagram below shows this flow. A value declared once at :root travels down to every branch of the tree. A branch that does not redeclare the property simply receives the value from its nearest ancestor that did declare it.
Overriding a Variable Locally
The right branch in the diagram is where custom properties become genuinely powerful for a designer, not just a programmer. Consider this addition to the earlier stylesheet:
.csk-card {
--brand-color: #f9cb08;
}
Nothing else needs to change. The rules for .card and .card__title from before still say var(--brand-color) — they have not been touched at all. But any card element that also carries the class csk-card now sits inside a part of the tree where --brand-color has been redefined to #f9cb08. Because custom properties inherit downward and each element uses the nearest declaration above it, every var(--brand-color) call inside that card resolves to yellow, while identical cards elsewhere on the page — that never redeclared the property — continue resolving to the blue set at :root.
This is the mechanism behind "theming" in real design systems: a designer defines one small set of named values (colours, spacing, radii) and different sections of a site, or different modes like light and dark, simply redeclare those names with new values on a wrapping element. None of the component rules — the actual borders, paddings, and colours — are duplicated or rewritten. Only the variable declarations change.
Fallback Values Inside var()
The var() function accepts an optional second argument, separated by a comma, which is used only if the named custom property has not been declared anywhere applicable to that element:
.card {
padding: var(--gap, 16px);
}
If --gap is defined somewhere up the tree, that value is used, and the fallback 16px is simply ignored. If --gap is anywhere the element can see, the browser falls back to 16px. Fallbacks are especially useful when you are writing a reusable component that might be dropped into a page that has not set up your custom properties at all — the component still renders with sensible spacing instead of breaking. Fallbacks can even be chained: var(--gap, var(--default-gap, 16px)) tries --gap first, then --default-gap, and only then falls back to the literal 16px.
It is worth being precise about what happens with no fallback at all and no declaration anywhere: the browser treats that specific declaration as invalid and simply ignores it, which usually means the property either keeps whatever value it would have inherited normally, or reverts to its default. In practice, this means you should always declare sane defaults for your custom properties at :root, so nothing is ever left by accident.
Combining with calc(): Building a Spacing Scale
Custom properties become even more useful when combined with CSS's calc() function, because a single base number can generate an entire consistent scale. Many real design systems (Google's Material Design is a well-known public example) build all of their spacing from one small base unit, multiplied by small whole numbers, rather than picking pixel values by eye for every element:
:root {
--unit: 8px;
}
.pad-sm { padding: calc(var(--unit) * 1); }
.pad-md { padding: calc(var(--unit) * 2); }
.pad-lg { padding: calc(var(--unit) * 3); }
Work through the arithmetic exactly as the browser does. --unit is bound to 8px. For .pad-sm, calc(var(--unit) * 1) substitutes to calc(8px * 1), which evaluates to 8px. For .pad-md, calc(8px * 2) evaluates to 16px. For .pad-lg, calc(8px * 3) evaluates to 24px. Every spacing value in your entire site is now a small multiple of one number. If a designer later decides the whole layout should feel slightly airier and changes --unit from 8px to 10px in one place, .pad-sm, .pad-md, and .pad-lg recompute to 10px, 20px, and 30px respectively — automatically, with zero further edits, exactly like changing x in an algebraic formula changes every expression that uses x.
The Big Misconception: "Isn't This the Same as a Sass Variable?"
Many students who have already seen a CSS preprocessor like Sass or LESS (which use $variable or @variable syntax) assume CSS custom properties are just the same idea with a different symbol. This is incorrect, and the difference is not cosmetic — it changes what you can actually do with them.
A Sass variable is resolved entirely before the browser ever runs. A build tool reads your .scss file, replaces every occurrence of $brand-color with its literal value, and writes out a plain .css file. Consider:
// Sass source
$brand-color: #0b5fff;
.card { border-color: $brand-color; }
/* Compiled CSS output — sent to the browser */
.card { border-color: #0b5fff; }
Look closely at the compiled output: $brand-color does not appear anywhere in it. The browser never receives the variable — only the final, literal colour value. This has a serious consequence: because the variable simply does not exist once the page is running, it cannot be changed by user interaction, by a media query responding to screen width, or by JavaScript. If you want a dark-mode toggle built with Sass variables alone, you must write out two entirely separate compiled stylesheets and switch between them.
A CSS custom property is fundamentally different: it is never "compiled away." It is a real, live entity inside the browser's rendering engine, participating in the cascade exactly like color or margin do. That means it can be redefined inside a media query for different screen sizes, redefined per element the way .csk-card did above, and — the most powerful difference — read and rewritten by JavaScript after the page has already loaded, without recompiling anything. This live, runtime behaviour is the entire reason the word "variable" is a slight understatement; a more accurate description is that a custom property is a small piece of state the stylesheet can react to.
A second, smaller misconception worth naming explicitly: writing color: --brand-color; directly, without wrapping it in var(), is invalid. The name --brand-color is not itself a colour value the way #0b5fff is — it is only a label pointing at a stored value, and var() is the function that performs the lookup. Forgetting var() is one of the most common first-week bugs, and the browser will not warn you loudly; the declaration is simply dropped as invalid.
Changing Variables Live, with JavaScript
Because custom properties are still "alive" in the browser, JavaScript can read and change them directly on any element, using the same style object you may already have used for other properties. This is exactly how real dark-mode toggles are built — the kind found in many Indian apps and websites that offer both a light and a dark theme for reading comfort at night, such as a ticket-booking or a payments interface.
:root {
--bg-color: #ffffff;
--text-color: #111111;
}
body {
background: var(--bg-color);
color: var(--text-color);
transition: background 0.3s, color 0.3s;
}
function toggleDarkMode() {
const root = document.documentElement;
const isDark = root.classList.toggle("dark");
root.style.setProperty(
"--bg-color",
isDark ? "#0f172a" : "#ffffff"
);
root.style.setProperty(
"--text-color",
isDark ? "#f1f5f9" : "#111111"
);
}
Trace what happens on a click. document.documentElement refers to the <html> element — the same element :root matches in CSS. classList.toggle("dark") adds the class dark if it is absent (returning true) or removes it if present (returning false); the returned boolean is stored in isDark. root.style.setProperty("--bg-color", ...) then writes a new value for --bg-color directly into the element's inline style. An inline style set this way takes precedence over the value declared in the stylesheet's :root rule, because inline styles are resolved with higher priority than selector-based rules in ordinary CSS (a stylesheet rule marked !important is the one exception, but that is not used here). The moment the new value is set, every element on the page that was reading var(--bg-color) or var(--text-color) — here, just body — recomputes automatically, and the transition rule already present in the CSS makes that recomputed change animate smoothly instead of snapping instantly. Nothing was recompiled; no new stylesheet was downloaded. The same CSS file is simply reading a different current value for the same named property, exactly the live, run-time behaviour a Sass variable can never offer.
Rules Worth Memorising
- A custom property name must begin with two hyphens (
--); a name with one hyphen or none is a real CSS property, not a custom one. - Custom property names are case-sensitive —
--Gapand--gapare unrelated. - A declared custom property does nothing on its own; it must be read with
var(--name)inside an actual property's value. - Custom properties inherit by default, unlike most ordinary CSS properties.
:rootis the conventional place to declare "global" custom properties, since every element in the document descends from it.- Custom properties can be redeclared on any selector to scope a new value to that subtree only.
var()accepts an optional fallback as its second argument.- Unlike Sass or LESS variables, custom properties survive into the running page and can be changed by media queries or JavaScript.
Where This Fits in Your CSS Foundation
Board-level Computer Science and Informatics units on web technologies typically ask you to read a short HTML/CSS snippet and predict rendered output, or to spot the one line that is invalid. Custom properties are a natural place to test exactly that skill, because the two most common bugs — omitting var(), and mismatching capitalisation of a property name — produce no visible error message, only a silently wrong result, which is precisely the kind of "trace it carefully" question board exams favour. Beyond exams, understanding that a value can be named once and consumed everywhere is the same discipline you will meet again as a named constant in Python or Java, or as a cell reference in a spreadsheet formula — CSS custom properties are simply that idea applied to a stylesheet.
Practice: Trace the Code Yourself
- Given
:root { --size: 10px; }and a rule.icon { width: calc(var(--size) * 4); }, what exact pixel width is computed for an element with classicon? - A student writes
.box { Color: var(--Main-Color); }after declaring--main-color: teal;at:root. Will the text render teal? Explain why, referring to case sensitivity. - A component rule uses
padding: var(--space, 12px), but the page that imports this component never declares--spaceanywhere. What padding is applied, and why? - Explain, in one or two sentences, why a Sass variable used to set a button colour cannot respond to a user clicking a "Switch to dark mode" button, while a CSS custom property can.
- If
.parent { --tone: navy; }and inside it sits.child { --tone: crimson; }, and a grandchild of.childusescolor: var(--tone)with no further redeclaration, what colour renders, and why?
Answers: (1) calc(10px * 4) evaluates to 40px. (2) No — --Main-Color and --main-color are different custom properties because names are case-sensitive, so the lookup finds nothing declared and the fallback-less var() reference is invalid; also note Color as a standard property name is fine since those are case-insensitive, but that does not rescue the custom-property mismatch. (3) 12px is applied, because --space is anywhere the element can see, so var() falls back to its second argument. (4) A Sass variable is deleted during compilation and replaced by a literal value before the browser runs, so no variable exists at click-time for JavaScript to change; a custom property still exists inside the running page and can be rewritten with setProperty(). (5) crimson renders, because the grandchild inherits the nearest ancestor declaration, which is .child's redefinition, not .parent's original value.
Summary
A CSS custom property is a named value, declared with a double-hyphen prefix inside any selector's rule block, and consumed elsewhere with the var() function — the same substitution idea you already know from algebra, where changing one bound value updates every expression that depends on it. Declaring a property at :root makes it available to the whole document because custom properties, unusually among CSS properties, inherit down the tree by default; redeclaring the same name on a more specific selector overrides it for just that subtree, which is the mechanism behind theming without duplicating component rules. var() supports a fallback value for safety, and combines naturally with calc() to build consistent numeric scales from one base unit. The property that separates custom properties from preprocessor variables like Sass's $variable is survival: a Sass variable is erased at build time and never reaches the browser, while a custom property remains live inside the running page, meaning it can be redefined per breakpoint and, critically, read or rewritten directly by JavaScript after the page has loaded — which is exactly how real light/dark theme switches are built without reloading or recompiling any stylesheet.
Think About It
Think about this: How would you explain css custom properties: variables for designers to a friend who has never seen a computer? What real-world analogy would you use? Imagine you had to build a system using these concepts — what would be your first step? Try this: before moving on, write down three things you learned and one question you still have.