Custom Properties
CSS custom properties (also called CSS variables) are reusable, named values you define with a --name prefix and read back with var() ; they cascade and inherit like normal CSS, so changing one value can re-theme an entire page.
Part of the free HTML & CSS course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
By the end of this lesson you'll be able to define live CSS variables, theme a whole page by changing one value, and read or update those variables from JavaScript — the foundation every modern design system is built on.
What You'll Learn
💡 Think of It Like This
A custom property is like a paint swatch labelled at the hardware store. Instead of writing "Pantone 2728 C" on every wall in the plans, you label one swatch --brand-blue and point at that label everywhere.
When the client changes their mind, you repaint one swatch and every room updates by itself — no hunting through forty walls. That single point of change is the whole idea, and because the swatch is a live label (not a one-time copy), you can even swap it while the paint is still wet: that is what lets JavaScript and themes change your colours after the page has loaded.
1. Declaring & Using a Variable
A custom property (the official name for a "CSS variable") is any property whose name starts with two dashes: --name: value; . You read it back anywhere a value is expected with the var(--name) function. The name is case-sensitive, so --Primary and --primary are two different properties.
Concept
Syntax
Example
Declare (global)
:root { --name: value; }
--primary: #3b82f6;
Declare (scoped)
.card { --pad: 20px; }
Only inside .card
Use
var(--name)
color: var(--primary);
Use with fallback
var(--name, fallback)
var(--accent, #f59e0b)
Run the worked example. Every colour, radius, and spacing value comes from one block of variables at the top. Change a single value there and watch the whole design move with it.
2. Fallbacks — A Safety Net for var()
var() takes an optional second argument: a value to use when the variable is undefined or out of scope. So color: var(--accent, #f59e0b); means "use --accent if it exists, otherwise fall back to amber." This is what stops a typo or a missing variable from leaving an element completely unstyled.
It matters because a var() that resolves to nothing makes the whole declaration invalid — the browser throws it away, and your element loses that style entirely. A fallback turns a silent disappearance into a sensible default.
3. Scope, Inheritance & the Cascade
Custom properties are not magic globals — they obey the same rules as every other CSS property. A variable is scoped to the selector it's declared on and inherits down to that element's descendants. Declare it on :root (which matches < html > ) and it's available to the whole page. Declare it on .card and only .card and what's inside it can see that value.
Because they follow the cascade, you can override a variable lower down the tree: a component can redefine --primary for itself without touching the global one. The value is resolved where it's used , so the same var(--primary) can produce different results in different parts of the page.
4. Theming — Swap One Attribute
Theming is the payoff. Define one set of variables for the light theme and another for the dark theme, keyed off an attribute like data-theme on the < html > element. Every component already reads var(--bg) , var(--text) and friends, so flipping that one attribute re-themes the entire page — no per-element JavaScript, no duplicated rules.
5. Reading & Setting Variables in JavaScript
Because custom properties are live , JavaScript can read and change them at runtime — something Sass variables can never do. To set one: element.style.setProperty('--name', value) . To read the resolved value: getComputedStyle(el).getPropertyValue('--name') .
Set the variable on document.documentElement (the < html > element, i.e. :root ) and it changes globally, because :root variables inherit everywhere. Every rule that uses var(--name) updates the instant you set it.
🎯 Your Turn #1 — Declare and use a variable
The colours below are hard-coded three times. Pull them into a variable so a single change re-themes the card. Fill in the blanks marked ___ , then run it and check the expected result in the comments.
🎯 Your Turn #2 — Add a fallback and scope an override
One element uses a variable that was never declared, and one component needs its own colour. Add a var() fallback and a scoped override to fix both, then verify the expected colours in the comments.
🧩 Mini-Challenge — Theme tokens from scratch
Support is faded now — only an outline is given. Build a small page that uses a variable design system, with one scoped override. Use the worked examples in sections 1 and 3 as your reference if you get stuck.
⚠️ Common Errors (and the fix)
- No fallback, variable undefined. color: var(--accent); when --accent doesn't exist makes the whole declaration invalid, so the element loses that style silently. Fix: give it a fallback — var(--accent, #f59e0b) .
- Expecting Sass-style compile-time behaviour. Custom properties are not find-and-replace like $blue in Sass — you can't use them in a selector, a media-query condition, or do @media (--mobile) . Fix: use var() only inside a property's value ; for conditional logic, write real media queries.
- Scope confusion. Declaring --gap on .card and then using it on a sibling outside .card resolves to nothing — the variable only inherits to descendants. Fix: declare site-wide tokens on :root so every element can see them.
- Typo / wrong case. var(--Primary) won't find --primary — names are case-sensitive and must match exactly. Fix: keep names lowercase-with-dashes and copy them carefully.
- Missing the double dash. Writing primary: #3b82f6; (one or no dash) declares an unknown property that the browser ignores. Fix: custom properties must start with two dashes: --primary .
📋 Quick Reference
Goal
Use this
Declare a global variable
:root { --primary: #3b82f6; }
Declare a scoped variable
Use a variable
Use with a fallback
Override per-component
.danger { --primary: #ef4444; }
Theme by attribute
[data-theme="dark"] { --bg: #0f172a; }
Set from JavaScript
el.style.setProperty('--hue', '120')
Read from JavaScript
getComputedStyle(el).getPropertyValue('--hue')
❓ Frequently Asked Questions
🎉 Lesson Complete
You can now build maintainable, themeable CSS with custom properties. The essentials:
- ✅ Declare with --name: value; and read with var(--name)
- ✅ Always add a fallback — var(--name, fallback) — so a missing variable can't break a rule
- ✅ :root = global; a selector = scoped, inheriting only to descendants
- ✅ Variables are live — they follow the cascade and can be overridden per-component
- ✅ Swap one data-theme attribute to re-theme the whole page
- ✅ Read and set them at runtime with getPropertyValue / setProperty
Next up: Web Accessibility , where you'll make every page usable for keyboard and screen-reader users.
Practice quiz
How do you declare a CSS custom property?
- $name: value;
- @name: value;
- --name: value;
- var-name: value;
Answer: --name: value;. Custom properties start with two dashes: --name: value;
How do you read back a custom property's value?
- var(--name)
- get(--name)
- value(--name)
- $(--name)
Answer: var(--name). The var() function substitutes a custom property's value: var(--name).
What does the second argument in var(--accent, #f59e0b) do?
- Sets the variable
- Defines a second variable
- Specifies the unit
- Acts as a fallback if --accent is undefined
Answer: Acts as a fallback if --accent is undefined. The second argument is a fallback used when the variable is undefined or out of scope.
Where should you declare site-wide global variables?
- body
- :root
- *
- @global
Answer: :root. :root matches the <html> element, so variables declared there inherit across the whole document.
What happens if var(--accent) resolves to nothing and has no fallback?
- The whole declaration becomes invalid and is ignored
- The element uses black
- The browser throws an error
- It uses the parent's value
Answer: The whole declaration becomes invalid and is ignored. A var() that resolves to nothing makes the declaration invalid, so the browser discards that style.
Are custom property names case-sensitive?
- No, --Primary equals --primary
- Only in Firefox
- Yes, --Primary and --primary are different
- Only inside :root
Answer: Yes, --Primary and --primary are different. Custom property names are case-sensitive: --Primary and --primary are two distinct properties.
If you declare --pad on .card, where is that variable available?
- The whole document
- Only .card and its descendants
- Only the .card element itself
- Every sibling of .card
Answer: Only .card and its descendants. A variable is scoped to its selector and inherits down to that element's descendants only.
How do you set a custom property from JavaScript?
- element.setVar('--name', value)
- element.css('--name', value)
- element.variable('--name', value)
- element.style.setProperty('--name', value)
Answer: element.style.setProperty('--name', value). element.style.setProperty('--name', value) sets a custom property at runtime.
What is a key difference between a CSS custom property and a Sass variable?
- Sass variables inherit; CSS ones don't
- CSS custom properties are live and changeable at runtime
- They are identical
- Sass variables work in JavaScript
Answer: CSS custom properties are live and changeable at runtime. CSS custom properties are live values the browser keeps and JS can change; Sass variables are compiled away.
Which is a valid use of a custom property?
- @media (--mobile)
- var(--selector) { ... }
- margin: var(--space-md)
- --name as a property name
Answer: margin: var(--space-md). var() only substitutes inside a property's value, so margin: var(--space-md) is valid.
Continue this course
- Previous: CSS Transforms & 3D
- Next: Web Accessibility