CSS Reference Guide

All TopicsPlayground

CSS Variables

Custom properties, :root scope, dynamic theming, fallback values, and JavaScript integration — the key to maintainable, dynamic stylesheets.

Custom Properties Theming var() :root Dynamic Styles

What Are CSS Custom Properties

CSS Custom Properties (commonly called CSS Variables) let you store values in named tokens that can be reused throughout your stylesheet. Unlike preprocessor variables (Sass $color, Less @color), CSS variables are live — they exist in the browser, cascade like any other property, inherit down the DOM tree, and can be changed at runtime with JavaScript.

CSS
/* Declare a custom property */
:root {
  --primary: #6c8cff;
}

/* Use it anywhere */
.button {
  background: var(--primary);
}

CSS Variables vs Preprocessor Variables

FeatureCSS VariablesSass / Less
Runs inThe browser (live)Build step (compiled away)
Cascade & inheritYesNo
Change at runtimeYes (JS, media queries, hover)No
Scoped to selectorsYes — any selectorGlobal or block scope only
DevTools inspectionVisible and editableCompiled out
Fallback valuesBuilt-in with var()Requires mixin logic
Key Insight
Because CSS variables are live in the browser, you can change a single variable and every element using it updates instantly — no rebuild needed.

Declaring Variables

A custom property starts with two dashes (--) followed by a name. The name is case-sensitive and can contain letters, numbers, hyphens, and underscores.

CSS
/* Valid declarations */
.element {
  --color-primary: #6c8cff;
  --font-size-lg: 1.25rem;
  --spacing-4: 16px;
  --myComponent-bg: hsl(220, 15%, 12%);
  --border_width: 2px;
  --123: red;  /* valid but not recommended */
}

/* Case-sensitive! These are TWO different variables */
.box {
  --myColor: red;
  --mycolor: blue;  /* different variable */
}

Where You Can Declare

Variables can be declared in any CSS selector — not just :root. They follow the same cascade and specificity rules as regular properties.

CSS
/* Global scope */
:root      { --gap: 16px; }

/* Element scope */
.card      { --gap: 24px; }

/* State scope */
.card:hover { --gap: 32px; }

/* Media query scope */
@media (min-width: 768px) {
  :root { --gap: 24px; }
}

/* Inline style */
/* <div style="--gap: 40px"> */

The :root Scope

The :root pseudo-class matches the document's root element (<html> in HTML). Because every element in the page inherits from it, variables declared on :root are effectively global.

:root vs html
:root and html both select the same element, but :root has higher specificity (0,1,0 vs 0,0,1). By convention, global variables go on :root.

Organized :root Block

CSS
:root {
  /* ── Colors ── */
  --color-primary: #6c8cff;
  --color-secondary: #a78bfa;
  --color-success: #2ecc71;
  --color-danger: #e74c3c;
  --color-text: #e2e4e9;
  --color-muted: #9ca3af;

  /* ── Spacing ── */
  --space-1: 4px;
  --space-2: 8px;
  --space-3: 12px;
  --space-4: 16px;
  --space-6: 24px;
  --space-8: 32px;

  /* ── Typography ── */
  --font-body: 'Inter', system-ui, sans-serif;
  --font-code: 'Fira Code', monospace;
  --font-size-sm: 0.875rem;
  --font-size-base: 1rem;
  --font-size-lg: 1.25rem;
  --font-size-xl: 1.5rem;

  /* ── Layout ── */
  --radius: 8px;
  --sidebar-w: 280px;
  --header-h: 58px;
  --transition: 0.25s ease;
}
Live Example — Variables in Action
--primary
--secondary
--success
--danger

Using var()

The var() function retrieves the value of a custom property. It can be used anywhere a value is expected in a CSS property.

CSS
.card {
  /* Simple usage */
  color: var(--color-text);
  padding: var(--space-4);
  border-radius: var(--radius);

  /* Inside other functions */
  background: hsl(var(--hue), 80%, 60%);
  width: calc(100% - var(--sidebar-w));
  transition: all var(--transition);
  font-family: var(--font-body);

  /* In shorthand properties */
  margin: var(--space-4) var(--space-6);
  border: 1px solid var(--border-color);
}

Using in calc()

CSS
:root {
  --base-size: 16px;
  --scale: 1.25;
}

h1 { font-size: calc(var(--base-size) * var(--scale) * var(--scale) * var(--scale)); }
h2 { font-size: calc(var(--base-size) * var(--scale) * var(--scale)); }
h3 { font-size: calc(var(--base-size) * var(--scale)); }
Live Example — Variables in calc()

Heading 1 — scale^3

Heading 2 — scale^2
Heading 3 — scale^1

Body text — base size

Fallback Values

The second argument to var() is a fallback value used when the variable is not defined or is invalid.

CSS
/* Simple fallback */
color: var(--text-color, #333);

/* Fallback to another variable */
color: var(--link-color, var(--primary, blue));

/* Fallback with commas (entire rest is the fallback) */
font-family: var(--font-stack, Inter, Helvetica, sans-serif);

/* The comma separates variable name from fallback */
/* Everything after the first comma is the fallback */
background: var(--bg, linear-gradient(to right, #6c8cff, #a78bfa));
Live Example — Fallback in Action
No --fb-color set (fallback: blue)
--fb-color: red
--fb-color: green
Comma Gotcha
Everything after the first comma is the fallback. So var(--font, Inter, sans-serif) has a fallback of Inter, sans-serif (including the comma). This is actually useful for font stacks!

Scope & Inheritance

CSS variables follow the same cascade and inheritance rules as regular CSS properties. A variable set on a parent is available to all its descendants. A variable set on a more specific selector overrides the inherited value for that subtree.

CSS
:root     { --color: blue; }
.sidebar  { --color: green; }   /* overrides for .sidebar subtree */
.alert    { --color: red; }     /* overrides for .alert subtree */

/* All these use var(--color) but get different values */
.text { color: var(--color); }
Live Example — Scoping & Inheritance
Inherited (blue)
Overridden (green)
Child inherits green
Overridden (red)
Overridden (purple)
Inheritance vs Cascade
If a variable is set on .parent and also on .parent .child, the child's declaration wins for the child and its descendants. This is standard CSS cascade behavior applied to custom properties.

Dynamic Theming

CSS variables make theming trivial. Define all your design tokens as variables, then swap them by changing a single attribute or class on the root element.

CSS
/* Dark theme (default) */
[data-theme="dark"] {
  --bg: #0f1117;
  --bg-card: #1a1d27;
  --text: #e2e4e9;
  --border: #2a2d3a;
  --accent: #6c8cff;
}

/* Light theme */
[data-theme="light"] {
  --bg: #f5f6fa;
  --bg-card: #ffffff;
  --text: #1a1d27;
  --border: #e0e2e9;
  --accent: #4a6cf7;
}

/* Components just use the variables */
.card {
  background: var(--bg-card);
  color: var(--text);
  border: 1px solid var(--border);
}
JavaScript
// Toggle theme
function toggleTheme() {
  const html = document.documentElement;
  html.dataset.theme = html.dataset.theme === 'dark' ? 'light' : 'dark';
  localStorage.setItem('theme', html.dataset.theme);
}
Live Example — Mini Theme Switcher

Themed Card

This card switches between dark and light by changing CSS variables on the container. Click the button!

Component-Level Variables

Scope variables to individual components to create self-contained, configurable UI elements. This is the pattern behind many modern CSS component libraries.

CSS
.btn {
  /* Component-level defaults */
  --btn-bg: #6c8cff;
  --btn-text: #fff;
  --btn-radius: 6px;
  --btn-padding: 8px 20px;

  background: var(--btn-bg);
  color: var(--btn-text);
  border-radius: var(--btn-radius);
  padding: var(--btn-padding);
  border: none;
  cursor: pointer;
  font-weight: 600;
}

/* Variants just override the variables */
.btn--danger  { --btn-bg: #e74c3c; }
.btn--success { --btn-bg: #2ecc71; }
.btn--outline {
  --btn-bg: transparent;
  --btn-text: #6c8cff;
  border: 2px solid var(--btn-text);
}
.btn--lg     { --btn-padding: 12px 32px; --btn-radius: 10px; }
Live Example — Button Variants

Variables with calc()

Combining variables with calc() lets you build dynamic spacing, sizing, and layout systems from a set of base tokens.

CSS
:root {
  --space-unit: 8px;
}

.box {
  padding: calc(var(--space-unit) * 2);    /* 16px */
  margin:  calc(var(--space-unit) * 3);    /* 24px */
  gap:     calc(var(--space-unit) * 1.5);  /* 12px */
}
Live Example — Spacing Scale
--space-1: 4px
--space-2: 8px
--space-3: 12px
--space-4: 16px
--space-6: 24px
--space-8: 32px
--space-12: 48px
--space-16: 64px

Variables & Media Queries

You can redefine variable values inside media queries to create responsive designs without duplicating properties.

CSS
:root {
  --container-padding: 16px;
  --font-size-hero: 1.8rem;
  --grid-columns: 1;
}

@media (min-width: 768px) {
  :root {
    --container-padding: 32px;
    --font-size-hero: 2.5rem;
    --grid-columns: 2;
  }
}

@media (min-width: 1200px) {
  :root {
    --container-padding: 48px;
    --font-size-hero: 3.5rem;
    --grid-columns: 3;
  }
}

/* Components automatically respond */
.container { padding: var(--container-padding); }
.hero h1   { font-size: var(--font-size-hero); }
.grid      { grid-template-columns: repeat(var(--grid-columns), 1fr); }
Pro Tip
By changing variables in media queries rather than rewriting property declarations, you keep your component CSS clean and change responsive behavior in one central place.
Limitation
You cannot use CSS variables inside the media query condition itself. This does NOT work: @media (min-width: var(--breakpoint)). The condition is evaluated before custom properties are resolved. Use environment variables (env()) or preprocessor variables for that.

JavaScript Integration

One of the most powerful features of CSS variables is that JavaScript can read and write them at runtime, enabling dynamic, interactive styling.

Reading Variables

JavaScript
// Read from :root / computed style
const root = document.documentElement;
const accent = getComputedStyle(root)
  .getPropertyValue('--accent');

// Read from a specific element
const card = document.querySelector('.card');
const cardBg = getComputedStyle(card)
  .getPropertyValue('--card-bg');

Writing Variables

JavaScript
// Set on :root (affects everything)
document.documentElement.style
  .setProperty('--accent', '#ff6b6b');

// Set on a specific element
card.style.setProperty('--card-bg', '#2a2d3a');

// Remove (revert to inherited/default)
card.style.removeProperty('--card-bg');
Live Example — Interactive Color Changer

Mouse-Tracking Example

JavaScript
document.addEventListener('mousemove', (e) => {
  document.documentElement.style.setProperty(
    '--mouse-x', e.clientX + 'px'
  );
  document.documentElement.style.setProperty(
    '--mouse-y', e.clientY + 'px'
  );
});

/* CSS */
.spotlight {
  background: radial-gradient(
    circle at var(--mouse-x) var(--mouse-y),
    rgba(108,140,255,.2), transparent 300px
  );
}

Animation & @property

By default, CSS variables cannot be animated with transitions or keyframes because the browser doesn't know what type of value they hold. The @property rule fixes this by registering a custom property with a specific syntax (type).

The Problem

CSS
/* This WON'T animate smoothly — it snaps */
:root { --hue: 0; }
.box {
  background: hsl(var(--hue), 80%, 60%);
  transition: --hue 1s; /* browser ignores this */
}
.box:hover { --hue: 180; }

The Solution: @property

CSS
@property --hue {
  syntax: '<number>';
  initial-value: 0;
  inherits: false;
}

.box {
  background: hsl(var(--hue), 80%, 60%);
  transition: --hue 1s ease; /* NOW it works! */
}
.box:hover { --hue: 180; }

@property Syntax Types

Syntax StringAcceptsExample
<number>Any number42, 3.14
<integer>Whole numbers1, 200
<length>Length values16px, 2rem
<percentage>Percentages50%
<color>Color values#ff0, rgb(…)
<angle>Angles45deg, 1turn
<length-percentage>Length or %10px, 50%
*Any value(no animation)
Live Example — Animated Gradient with @property

The conic-gradient above animates smoothly because --ap-angle is registered with @property as <angle>.

Practical Patterns

Design Token System

CSS
:root {
  /* ── Primitive tokens ── */
  --blue-50:  #eff6ff;
  --blue-500: #3b82f6;
  --blue-900: #1e3a5f;
  --gray-100: #f3f4f6;
  --gray-800: #1f2937;

  /* ── Semantic tokens (reference primitives) ── */
  --color-primary: var(--blue-500);
  --color-bg:      var(--gray-100);
  --color-text:    var(--gray-800);

  /* ── Component tokens (reference semantic) ── */
  --btn-bg:      var(--color-primary);
  --card-bg:     var(--color-bg);
  --heading-color: var(--color-text);
}

Z-Index Management

CSS
:root {
  --z-dropdown: 100;
  --z-sticky:   200;
  --z-overlay:  300;
  --z-modal:    400;
  --z-toast:    500;
  --z-tooltip:  600;
}

.modal   { z-index: var(--z-modal); }
.tooltip { z-index: var(--z-tooltip); }

Responsive Typography Scale

CSS
:root {
  --text-xs:   clamp(0.7rem,  0.65rem + 0.25vw, 0.8rem);
  --text-sm:   clamp(0.8rem,  0.75rem + 0.3vw,  0.9rem);
  --text-base: clamp(0.9rem,  0.85rem + 0.35vw, 1.05rem);
  --text-lg:   clamp(1.1rem,  1rem + 0.5vw,    1.35rem);
  --text-xl:   clamp(1.3rem,  1.1rem + 0.8vw,  1.8rem);
  --text-2xl:  clamp(1.6rem,  1.3rem + 1.2vw,  2.5rem);
  --text-3xl:  clamp(2rem,    1.5rem + 2vw,    3.5rem);
}

Color Palette from HSL

CSS
:root {
  --hue: 220;
  --primary-50:  hsl(var(--hue), 90%, 95%);
  --primary-100: hsl(var(--hue), 85%, 85%);
  --primary-300: hsl(var(--hue), 80%, 70%);
  --primary-500: hsl(var(--hue), 75%, 55%);
  --primary-700: hsl(var(--hue), 70%, 40%);
  --primary-900: hsl(var(--hue), 65%, 25%);
}

/* Change ONE variable to shift the entire palette! */
Live Example — HSL Palette Generator
50
100
200
300
400
500
600
700
800
900

Gotchas

1. Variables Can't Be Used in Property Names
var(--prop-name): value; does NOT work. Variables can only replace values, not property names or selectors.
2. Cyclic Dependencies
If --a references --b and --b references --a, both become invalid at computed-value time. The browser treats them as if they were never set and falls back to the property's initial value (not the var() fallback).
3. Invalid at Computed-Value Time
If a variable holds a value that's invalid for the property it's used in (e.g., --size: red; width: var(--size)), the property gets the initial value — not the inherit or fallback. This is different from a normal syntax error!
4. Can't Use in Media Query Conditions
@media (min-width: var(--bp)) does NOT work. Media conditions are evaluated before custom properties are resolved. Use preprocessor variables or hard-code breakpoints.
5. Case Sensitivity
--myColor and --mycolor are different variables. This is unlike regular CSS properties which are case-insensitive. Stick to a consistent naming convention (e.g., kebab-case).
6. Empty Variables Are Valid
--empty: ; (with a space after the colon) is a valid declaration. This can cause hard-to-debug issues where the variable exists but holds whitespace.

Pro Tips

1. Naming Conventions
Use a consistent system. Popular patterns:
  • By category: --color-primary, --font-size-lg, --space-4
  • By component: --btn-bg, --card-radius, --nav-height
  • Layered tokens: primitive → semantic → component (like the design token pattern above)
2. Debug in DevTools
In Chrome/Firefox DevTools, custom properties appear in the Computed tab. You can edit them live in the Elements panel. Firefox's DevTools even show a color swatch for variables that hold color values.
3. Use Unitless Values for Flexibility
Store unitless numbers and add units with calc(): --columns: 3; grid-template-columns: repeat(var(--columns), 1fr); — This makes it easy to change via JS without worrying about units.
4. Component API Pattern
Document which variables a component exposes. This creates a clean customization API:
CSS
/*
 * .alert Component API:
 * --alert-bg      Background color  (default: #fff3cd)
 * --alert-border  Border color      (default: #ffc107)
 * --alert-text    Text color        (default: #664d03)
 * --alert-radius  Corner radius     (default: 8px)
 */
.alert {
  background: var(--alert-bg, #fff3cd);
  border: 1px solid var(--alert-border, #ffc107);
  color: var(--alert-text, #664d03);
  border-radius: var(--alert-radius, 8px);
  padding: 12px 16px;
}
5. Performance
CSS variables are very fast. The browser resolves them during style computation, similar to how it resolves inherit. Even hundreds of variables have negligible performance impact. The only caveat: animating variables without @property triggers a full style recalculation.
6. Browser Support
CSS Custom Properties are supported in all modern browsers (Chrome 49+, Firefox 31+, Safari 9.1+, Edge 15+). @property has more limited support (Chrome 85+, Safari 15.4+, Firefox 128+). Use @supports for progressive enhancement.
Previous Pseudo-classes & Elements