Skip to main content
    Courses/HTML & CSS/CSS Custom Properties

    Lesson 14 • Expert Track

    CSS Custom Properties

    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

    Declare a custom property with --name and read it with var()
    Add a fallback so a missing variable can't break your layout
    Tell global :root variables from locally scoped ones
    Use inheritance and the cascade to override variables per-component
    Build a light/dark theme by swapping one attribute
    Read and set variables live from JavaScript with style.setProperty
    Before this lesson: you should be comfortable writing CSS rules and selectors, and know what the cascade and inheritance mean. If selectors, specificity, or :root feel new, review CSS Basics & Selectors first.

    💡 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.

    ConceptSyntaxExample
    Declare (global):root { --name: value; }--primary: #3b82f6;
    Declare (scoped).card { --pad: 20px; }Only inside .card
    Usevar(--name)color: var(--primary);
    Use with fallbackvar(--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.

    Worked example: one source of truth

    Every style reads from variables declared once in :root

    Try it Yourself »
    Code Preview
    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <title>CSS Variables</title>
      <style>
        :root {
          /* Declare design tokens once. Change any line to retheme everything. */
          --primary: #3b82f6;        /* brand colour, reused everywhere below */
          --primary-dark: #2563eb;   /* a darker shade for hover states */
          --bg: #0f172a;             /* page background */
          --surface: #1e293b;        /* card background */
          --text: #e5e7eb;           /* body text
    ...

    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.

    Worked example: with and without a fallback

    See what happens when a variable is never declared

    Try it Yourself »
    Code Preview
    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <title>var() fallbacks</title>
      <style>
        :root {
          --text: #e5e7eb;
          --primary: #3b82f6;
          /* NOTE: --accent is deliberately NOT declared anywhere. */
        }
    
        body { background:#0f172a; color: var(--text); font-family: system-ui, sans-serif; padding:20px; }
        .box { padding:16px; border-radius:10px; margin:12px 0; background:#1e293b; }
    
        /* No fallback: --accent is undefined, so this declaration is INVALI
    ...

    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.

    Worked example: global vs scoped override

    The same var(--accent) resolves differently per component

    Try it Yourself »
    Code Preview
    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <title>Scope & inheritance</title>
      <style>
        :root {
          --accent: #3b82f6;   /* GLOBAL accent: blue, inherited by the whole page */
        }
    
        body { background:#0f172a; color:#e5e7eb; font-family: system-ui, sans-serif; padding:20px; }
    
        .panel {
          background:#1e293b; border-left:5px solid var(--accent);
          padding:16px; border-radius:10px; margin:12px 0;
        }
        .panel h3 { color: var(--accent); }   /* resolv
    ...

    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.

    Worked example: light/dark theme switch

    One attribute toggle swaps every colour on the page

    Try it Yourself »
    Code Preview
    <!DOCTYPE html>
    <html lang="en" data-theme="light">
    <head>
      <meta charset="UTF-8">
      <title>Theme switcher</title>
      <style>
        /* LIGHT theme (default): variables defined for the light data-theme. */
        [data-theme="light"] {
          --bg: #f8fafc; --surface: #ffffff; --text: #1e293b;
          --muted: #64748b; --primary: #3b82f6; --border: #e2e8f0;
        }
        /* DARK theme: same variable NAMES, different values. */
        [data-theme="dark"] {
          --bg: #0f172a; --surface: #1e293b; --text: #e5e7eb
    ...

    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.

    Worked example: a live colour slider

    JavaScript writes a variable; CSS reacts instantly

    Try it Yourself »
    Code Preview
    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <title>JS + CSS variables</title>
      <style>
        :root { --hue: 210; }   /* a number JavaScript will change */
        body { background:#0f172a; color:#e5e7eb; font-family: system-ui, sans-serif; padding:24px; }
    
        /* hsl() reads --hue, so changing one number re-tints everything. */
        .swatch {
          height:120px; border-radius:12px; margin:16px 0;
          background: hsl(var(--hue), 80%, 55%);
        }
        .label { font-size:14px; co
    ...

    🎯 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 #1: replace hard-coded colours with a variable

    Declare --brand in :root and read it with var()

    Try it Yourself »
    Code Preview
    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <title>Your Turn 1</title>
      <style>
        /* 🎯 YOUR TURN — fill in the blanks marked ___ */
    
        :root {
          /* 1) Declare a variable called --brand set to #8b5cf6 (purple). */
          ___                        /* 👉 add:  --brand: #8b5cf6; */
        }
    
        body { background:#0f172a; color:#e5e7eb; font-family: system-ui, sans-serif; padding:20px; }
    
        .card {
          background:#1e293b; padding:20px; border-radius:12px; max-width:
    ...

    🎯 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.

    Your Turn #2: fallback + scoped override

    Rescue an undefined variable and override one per-component

    Try it Yourself »
    Code Preview
    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <title>Your Turn 2</title>
      <style>
        /* 🎯 YOUR TURN — fill in the blanks marked ___ */
    
        :root {
          --text: #e5e7eb;
          --accent: #3b82f6;   /* global accent = blue */
          /* --warn is NOT declared on purpose. */
        }
    
        body { background:#0f172a; color: var(--text); font-family: system-ui, sans-serif; padding:20px; }
        .box { background:#1e293b; padding:16px; border-radius:10px; margin:12px 0; border-left:5p
    ...

    🧩 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.

    Mini-Challenge: variables from scratch

    A :root token set, var() everywhere, and one scoped override

    Try it Yourself »
    Code Preview
    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <title>Mini-Challenge</title>
      <style>
        /* 🧩 MINI-CHALLENGE: a tiny variable-driven design system
           1. In :root, declare these tokens:
                --bg (a dark colour), --surface (a card colour),
                --text (light), --primary (a brand colour), --radius (e.g. 12px)
           2. Style <body> using var(--bg) and var(--text)
           3. Style a .card using var(--surface), var(--radius), and a
                border-left in v
    ...

    ⚠️ 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

    GoalUse this
    Declare a global variable:root { --primary: #3b82f6; }
    Declare a scoped variable.card { --pad: 20px; }
    Use a variablecolor: var(--primary);
    Use with a fallbackvar(--accent, #f59e0b)
    Override per-component.danger { --primary: #ef4444; }
    Theme by attribute[data-theme="dark"] { --bg: #0f172a; }
    Set from JavaScriptel.style.setProperty('--hue', '120')
    Read from JavaScriptgetComputedStyle(el).getPropertyValue('--hue')

    ❓ Frequently Asked Questions

    What is the difference between a CSS custom property and a Sass variable?

    A Sass (or Less) variable is compiled away before the browser ever sees it — by the time your stylesheet loads, $blue has been replaced by a fixed colour, and nothing can change it at runtime. A CSS custom property is a real, live value the browser keeps around: it follows the cascade, it inherits, it responds to media queries, and JavaScript can read or change it on the fly. If you need a value that changes after the page loads — themes, user settings, animation — you need a custom property, not a Sass variable.

    Why does my var() do nothing — the element just looks unstyled?

    Almost always one of three things: (1) you misspelled the name, and names are case-sensitive — --Primary and --primary are different properties; (2) the variable was declared on an element that is not an ancestor of the one using it, so it is out of scope and resolves to nothing; or (3) the resolved value is invalid for that property (e.g. a colour variable used where a length is expected). Add a fallback — var(--primary, #3b82f6) — so a typo or out-of-scope reference degrades gracefully instead of vanishing.

    Where should I declare global variables?

    Put site-wide design tokens (brand colours, spacing scale, fonts) in the :root selector. :root matches the <html> element, the top of the document, so every other element inherits those variables and can use them with var(). Declare a variable on a more specific selector only when you deliberately want it to apply to that element and its descendants — that is scoping, and it is a feature, not a bug.

    Can I use a custom property for any value, like a media query or a property name?

    You can store almost any value — colours, lengths, numbers, even whole shorthand strings — and substitute it with var(). What you cannot do is use a custom property as a selector, a media-query condition, or a property name; var() only substitutes inside a property's value. So @media (--mobile) is invalid, but margin: var(--space-md) is fine.

    How do I change a CSS variable from JavaScript?

    Read it with getComputedStyle(element).getPropertyValue('--name') and set it with element.style.setProperty('--name', value). Setting it on document.documentElement (the <html> element) changes it globally because :root variables inherit everywhere. Because the variable is live, every rule that uses var(--name) updates the instant you set it — no need to touch any other style.

    Are CSS custom properties widely supported?

    Yes. Custom properties have been supported in every modern browser (Chrome, Firefox, Safari, Edge) for years and are safe to use in production today. Only very old browsers like Internet Explorer 11 lack support, and the fallback argument in var() plus a plain CSS declaration before it covers those edge cases.

    🎉 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 fallbackvar(--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.

    Sign up for free to track which lessons you've completed and get learning reminders.

    Previous

    Cookie & Privacy Settings

    We use cookies to improve your experience, analyze traffic, and show personalized ads. You can manage your preferences below.

    By clicking "Accept All", you consent to our use of cookies for analytics and personalized advertising. You can customize your preferences or reject non-essential cookies.

    Privacy PolicyTerms of Service