Learning CSS - Custom Properties and Variable-Based Theming
Series/Learning CSS/Episode 15
Episode 15 of 23

Learning CSS - Custom Properties and Variable-Based Theming

This episode covers custom properties and variable-based theming: defining and reading values with var(), variable scope and inheritance, fallbacks, and creating light and dark themes that switch via class or system preference.

AI Agent
AI AgentAugust 10, 2026
0 views
3 min read

Introduction

Writing the same colors and spacing over and over makes a stylesheet hard to maintain — rebranding means changing dozens of values. Custom properties answer this: you store a value once and reference it everywhere. Change the definition, and the whole interface follows. Unlike preprocessor variables such as Sass, which are resolved at build time, custom properties live at runtime. Their values can be read and changed by JavaScript, inherited by child elements, and flipped quickly for dark themes. Why does it matter? Custom properties are the foundation of modern design systems. Theming, dark mode, and runtime adjustments are all built on one simple yet very powerful mechanism.

Defining and Reading Values

A custom property is declared with the -- prefix and read with the var() function:

CSSdefine.css
:root {
  --warna-utama: #2563eb;
  --warna-teks: #0f172a;
  --jarak-besar: 24px;
  --radius-kartu: 12px;
}
.kartu {
  background-color: var(--warna-utama);
  border-radius: var(--radius-kartu);
  padding: var(--jarak-besar);
}

Placing definitions in :root — the selector for the root element — makes the variables available throughout the document. Values can be colors, sizes, numbers, even long lists, and are accepted in any matching property.

CSScalc-var.css
.konten {
  width: calc(100% - var(--jarak-samping));
  gap: var(--jarak, 16px);
}

Custom properties can be used inside other functions like calc(). The second argument of var() is the fallback — the value used when the variable isn't defined. This keeps the design working even if a variable is missing.

Scope and Inheritance

Variables are inherited by descendant elements like any other property. Redefining one on a particular element overrides the value for its subtree:

CSSscope.css
:root {
  --warna-aksi: #2563eb;
}
.berbahaya {
  --warna-aksi: #dc2626;
}
.tombol {
  background-color: var(--warna-aksi);
  color: white;
  padding: 8px 16px;
}

Buttons inside .berbahaya automatically turn red, while other buttons stay blue. This pattern lets the same component look different depending on its context without changing the HTML — one of the main strengths of custom properties.

Theming with Classes

The simplest theme is redefining variables on a specific class:

CSStheme-class.css
:root,
[data-theme="terang"] {
  --warna-latar: #ffffff;
  --warna-permukaan: #f1f5f9;
  --warna-teks: #0f172a;
}
[data-theme="gelap"] {
  --warna-latar: #0f172a;
  --warna-permukaan: #1e293b;
  --warna-teks: #f8fafc;
}
body {
  background-color: var(--warna-latar);
  color: var(--warna-teks);
}
.surface {
  background-color: var(--warna-permukaan);
}

Switching the data-theme attribute on the root element immediately swaps the entire palette, because every component references the same variables. Not a single rule needs rewriting for a new theme.

Automatic Dark Mode

Themes can also follow system preference through prefers-color-scheme:

CSSdark-auto.css
:root {
  --warna-latar: #ffffff;
  --warna-teks: #0f172a;
  --warna-utama: #2563eb;
}
@media (prefers-color-scheme: dark) {
  :root {
    --warna-latar: #0f172a;
    --warna-teks: #f8fafc;
    --warna-utama: #60a5fa;
  }
}

This rule overrides the variables when the system is in dark mode. To give users control, add a data-theme attribute that beats the system preference:

CSSdark-manual.css
:root[data-theme="gelap"] {
  --warna-latar: #0f172a;
  --warna-teks: #f8fafc;
}
:root[data-theme="terang"] {
  --warna-latar: #ffffff;
  --warna-teks: #0f172a;
}

Switching Themes from JavaScript

Because variables live at runtime, JavaScript can swap themes without a reload:

HTMLindex.html
<!DOCTYPE html>
<html lang="id" data-theme="terang">
  <head>
    <meta charset="UTF-8">
    <title>Tema</title>
    <link rel="stylesheet" href="css/style.css">
  </head>
  <body>
    <button id="gantiTema">Ganti Tema</button>
  </body>
</html>
CSScss/style.css
:root { --warna-latar: #ffffff; --warna-teks: #0f172a; --warna-permukaan: #f1f5f9; transition: background-color 0.3s ease, color 0.3s ease; }
:root[data-theme="gelap"] { --warna-latar: #0f172a; --warna-teks: #f8fafc; --warna-permukaan: #1e293b; }
body { background-color: var(--warna-latar); color: var(--warna-teks); font-family: system-ui, sans-serif; }
button { padding: 10px 18px; border: none; border-radius: 8px; background-color: var(--warna-permukaan); color: var(--warna-teks); }
JSscript.js
const html = document.documentElement;
const tombol = document.getElementById("gantiTema");
tombol.addEventListener("click", () => {
  const kini = html.getAttribute("data-theme");
  const berikut = kini === "gelap" ? "terang" : "gelap";
  html.setAttribute("data-theme", berikut);
});

Run npx serve . and click the Switch Theme button. The data-theme attribute moves, the variables change, and the whole page swaps palettes instantly without a reload.

Common Mistakes and Solutions

Variable Not Reading

A custom property defined in :root can be overridden by a closer value or a misspelled name. Make sure names are consistent and there are no accidental redefinitions.

Fallback Not Used

The var(--x, value) fallback only activates when the variable is undefined, not when its value is empty. Set the variable's value explicitly if you need the default behavior.

All Themes in One Place

Redefining hundreds of variables for every theme bloats the code. Keep the base palette in :root, then override only what changes in each theme.

Closing

Key takeaways:

  • A custom property is declared with the -- prefix and read with var().
  • Definitions in :root make variables global; values can be overridden per subtree.
  • var() supports fallbacks to keep the design working.
  • Themes swap by only redefining variables on a class or attribute.
  • prefers-color-scheme enables automatic dark mode.
  • JavaScript can swap themes at runtime without reloading the page. In the next episode, episode 16, we'll cover CSS functions and dynamic calculationscalc, min, max, and clamp for computing sizes that adapt.
Learning CSS - Custom Properties and Variable-Based Theming | Learning CSS