Home/Learn/HTML, CSS & Responsive/Custom Properties and Theming

Custom Properties and Theming

Intermediate
CSS Fundamentals

CSS variables are live values that inherit and cascade, which makes theming, dark mode and component variants a matter of redefining tokens rather than rewriting rules.

Overview

Preprocessor variables are compile-time substitutions; CSS custom properties are real values in the cascade. They inherit, they can be redefined for a subtree, they can be read and written from JavaScript, and they update live. That difference is what makes them the mechanism behind every modern theming system: define a set of tokens once, redefine the tokens under a dark-mode media query or a data attribute, and every rule that references them follows. No component CSS needs to know that a theme exists.

Tokens

Define at the root, reference everywhere, and layer semantic names over raw values.

Primitives, then semantic tokens
:root {
  /* primitives — the raw palette */
  --blue-600: #2563eb;
  --grey-900: #111827;

  /* semantic tokens — what the UI actually references */
  --color-accent: var(--blue-600);
  --color-text: var(--grey-900);
  --color-surface: #ffffff;

  --space-1: .25rem; --space-2: .5rem; --space-4: 1rem;
  --radius: .5rem;
  --shadow-sm: 0 1px 2px rgb(0 0 0 / .06);
}

.button { background: var(--color-accent); border-radius: var(--radius) }

/* Two layers matters: components reference MEANING, so swapping the
   palette changes one line rather than forty. */

/* Fallbacks for a possibly-undefined variable */
color: var(--color-text, #111)
padding: var(--card-padding, var(--space-4, 1rem))

/* They inherit, so a subtree can override */
.card { --color-accent: var(--green-600) }   /* only inside this card */

Dark Mode

Three states — system, explicit light, explicit dark — handled by redefining tokens only.

Redefine tokens, never component rules
/* 1. Light is the base */
:root {
  --bg: #ffffff; --text: #111827; --rule: #e5e7eb;
}

/* 2. System dark, unless the user explicitly chose light */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --bg: #0b0f14; --text: #e5e7eb; --rule: #1f2937;
  }
}

/* 3. An explicit choice wins in both directions */
:root[data-theme="dark"] {
  --bg: #0b0f14; --text: #e5e7eb; --rule: #1f2937;
}

body { background: var(--bg); color: var(--text) }

/* Every component now themes itself with no dark-mode rules at all.
   The bug to avoid: defining a colour ONLY inside the media query,
   so it is undefined for a user whose system is light. */

/* Set the toggle from JavaScript */
document.documentElement.dataset.theme = 'dark'

/* And tell the browser, so form controls and scrollbars follow */
:root { color-scheme: light dark }

Dynamic Values

Where custom properties do things a preprocessor variable cannot.

setProperty, @property, variant tokens
/* Read and write from JavaScript — live */
el.style.setProperty('--progress', '0.4')
getComputedStyle(el).getPropertyValue('--color-accent')

/* Which enables patterns that would otherwise need a class per value */
.bar { width: calc(var(--progress) * 100%) }
.card { transform: rotate(calc(var(--i) * 2deg)) }

/* Passing a value from HTML into CSS */
<div class="chart" style="--value: 72">
.chart::after { height: calc(var(--value) * 1%) }

/* @property gives a variable a type, so it can ANIMATE — plain
   custom properties cannot be interpolated */
@property --angle {
  syntax: '<angle>'; initial-value: 0deg; inherits: false;
}
.spinner { transition: --angle .3s; background: conic-gradient(…, var(--angle)) }

/* Component variants without variant-specific rules */
.button { background: var(--btn-bg, var(--color-accent)) }
.button--danger { --btn-bg: var(--color-danger) }

/* Scoped to a breakpoint — one variable, one media query, and every
   rule that uses it responds */
@media (min-width: 768px) { :root { --space-section: 6rem } }

Key Points to Remember

  • 1Custom properties are live cascade values, not compile-time substitutions — they inherit and can be redefined per subtree
  • 2Layer semantic tokens over raw primitives so a palette change touches one place
  • 3Theming means redefining tokens in three blocks — base, prefers-color-scheme, and an explicit data-theme
  • 4Defining a colour only inside a dark-mode media query leaves it undefined in light mode
  • 5They can be read and written from JavaScript, and @property gives them a type so they can animate

Interview Questions

Sign in to ask Aria
1

How do CSS custom properties differ from Sass variables?

Medium
2

How would you implement a dark mode that respects both the system setting and an explicit toggle?

Hard
3

Why can a plain custom property not be animated, and what fixes that?

Hard

Ask Aria about Custom Properties and Theming

Your personal AI tutor — ask anything about this concept

Revision Status

Personal Notes

Sign in to save personal notes for this topic.

Discussion

Sign in to join the discussion.

Loading discussion…