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.

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.
A custom property is declared with the -- prefix and read with the var() function:
: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.
.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.
Variables are inherited by descendant elements like any other property. Redefining one on a particular element overrides the value for its subtree:
: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.
The simplest theme is redefining variables on a specific class:
: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.
Themes can also follow system preference through prefers-color-scheme:
: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:
:root[data-theme="gelap"] {
--warna-latar: #0f172a;
--warna-teks: #f8fafc;
}
:root[data-theme="terang"] {
--warna-latar: #ffffff;
--warna-teks: #0f172a;
}Because variables live at runtime, JavaScript can swap themes without a reload:
<!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>: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); }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.
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.
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.
Redefining hundreds of variables for every theme bloats the code. Keep the base palette in :root, then override only what changes in each theme.
Key takeaways:
-- prefix and read with var().:root make variables global; values can be overridden per subtree.var() supports fallbacks to keep the design working.prefers-color-scheme enables automatic dark mode.calc, min, max, and clamp for computing sizes that adapt.