Skip to main content
Ganesh Joshi
Back to Blogs

CSS custom properties: a practical guide for themes and reuse

February 9, 20264 min read
Tutorials
Color palette and design on screen

CSS custom properties (variables) let you define values once and reuse or override them. They work in all modern browsers and are part of the CSS standard. No preprocessor required.

Basic syntax

Defining variables

Variables start with two dashes:

:root {
  --primary-color: #3b82f6;
  --spacing-unit: 8px;
  --font-sans: 'Inter', system-ui, sans-serif;
}

Using variables

Use var() to reference a variable:

.button {
  background: var(--primary-color);
  padding: var(--spacing-unit);
  font-family: var(--font-sans);
}

Fallback values

Provide a fallback if the variable is not defined:

.card {
  color: var(--text-color, #333);
  border-radius: var(--radius, 4px);
}

Scoping and inheritance

CSS custom properties cascade like other CSS properties:

:root {
  --accent: blue;
}

.card {
  --accent: green;
}

.button {
  background: var(--accent);
}
  • A .button in the document uses blue (from :root)
  • A .button inside .card uses green (inherited from .card)

This makes component-specific theming natural.

Theme patterns

Light and dark mode

Define variables for both themes:

:root {
  --bg: #ffffff;
  --text: #1a1a1a;
  --border: #e5e7eb;
  --accent: #3b82f6;
}

.dark {
  --bg: #1a1a1a;
  --text: #f5f5f5;
  --border: #374151;
  --accent: #60a5fa;
}

Use the variables everywhere:

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

.card {
  border: 1px solid var(--border);
}

.link {
  color: var(--accent);
}

Toggle themes by adding/removing the .dark class on <html> or <body>.

System preference

Use prefers-color-scheme:

:root {
  --bg: #ffffff;
  --text: #1a1a1a;
}

@media (prefers-color-scheme: dark) {
  :root {
    --bg: #1a1a1a;
    --text: #f5f5f5;
  }
}

Combine with a manual toggle:

:root {
  --bg: #ffffff;
  --text: #1a1a1a;
}

@media (prefers-color-scheme: dark) {
  :root:not(.light) {
    --bg: #1a1a1a;
    --text: #f5f5f5;
  }
}

.dark {
  --bg: #1a1a1a;
  --text: #f5f5f5;
}

.light {
  --bg: #ffffff;
  --text: #1a1a1a;
}

Spacing and typography systems

Spacing scale

Define a consistent spacing scale:

:root {
  --space-1: 4px;
  --space-2: 8px;
  --space-3: 12px;
  --space-4: 16px;
  --space-6: 24px;
  --space-8: 32px;
  --space-12: 48px;
  --space-16: 64px;
}

.card {
  padding: var(--space-4);
  margin-bottom: var(--space-6);
}

.section {
  padding: var(--space-12) var(--space-4);
}

Typography scale

:root {
  --text-xs: 0.75rem;
  --text-sm: 0.875rem;
  --text-base: 1rem;
  --text-lg: 1.125rem;
  --text-xl: 1.25rem;
  --text-2xl: 1.5rem;
  --text-3xl: 1.875rem;
  --text-4xl: 2.25rem;
}

h1 {
  font-size: var(--text-4xl);
}

.caption {
  font-size: var(--text-sm);
}

Responsive adjustments

Change variables at breakpoints:

:root {
  --container-padding: var(--space-4);
}

@media (min-width: 768px) {
  :root {
    --container-padding: var(--space-6);
  }
}

@media (min-width: 1024px) {
  :root {
    --container-padding: var(--space-8);
  }
}

.container {
  padding-inline: var(--container-padding);
}

One variable change affects all elements using it.

Using calc() with variables

Variables work with calc():

:root {
  --spacing: 8px;
}

.card {
  padding: calc(var(--spacing) * 2);
  margin: calc(var(--spacing) * 3);
}

.header {
  height: calc(var(--spacing) * 8);
}

Complex calculations

:root {
  --sidebar-width: 250px;
  --header-height: 64px;
  --gap: 16px;
}

.main {
  width: calc(100% - var(--sidebar-width) - var(--gap));
  min-height: calc(100vh - var(--header-height));
}

Component-scoped variables

Override variables per component:

.button {
  --button-bg: var(--primary-color);
  --button-text: white;
  --button-padding: var(--space-2) var(--space-4);

  background: var(--button-bg);
  color: var(--button-text);
  padding: var(--button-padding);
}

.button.secondary {
  --button-bg: transparent;
  --button-text: var(--primary-color);
}

.button.large {
  --button-padding: var(--space-3) var(--space-6);
}

This pattern makes variants clean and maintainable.

JavaScript interaction

Read and write CSS variables with JavaScript:

// Read a variable
const root = document.documentElement;
const primaryColor = getComputedStyle(root).getPropertyValue('--primary-color');

// Set a variable
root.style.setProperty('--primary-color', '#10b981');

// Set on a specific element
const card = document.querySelector('.card');
card.style.setProperty('--accent', '#f59e0b');

This enables dynamic theming, user preferences, and animations.

Common patterns

Color with opacity

Define colors without opacity, then apply it:

:root {
  --primary-rgb: 59, 130, 246;
}

.overlay {
  background: rgb(var(--primary-rgb) / 0.5);
}

.hover-bg:hover {
  background: rgb(var(--primary-rgb) / 0.1);
}

Animation values

:root {
  --transition-fast: 150ms ease;
  --transition-normal: 300ms ease;
  --transition-slow: 500ms ease;
}

.button {
  transition: all var(--transition-fast);
}

.modal {
  transition: opacity var(--transition-normal);
}

Z-index scale

:root {
  --z-dropdown: 100;
  --z-modal: 200;
  --z-tooltip: 300;
  --z-toast: 400;
}

.dropdown {
  z-index: var(--z-dropdown);
}

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

Best practices

Practice Why
Define on :root for globals Accessible everywhere
Use semantic names --primary-color not --blue
Group related variables Colors, spacing, typography
Provide fallbacks Handle undefined variables
Keep values simple Colors, lengths, numbers work best
Document your system Help team members use correctly

CSS variables vs preprocessor variables

Aspect CSS variables Sass/Less variables
Runtime Yes No (compiled away)
JavaScript access Yes No
Dynamic theming Yes No
Cascade/inherit Yes No
Browser support Modern browsers Any (compiles to plain CSS)

Use CSS variables for runtime theming and values that change. Use preprocessor variables for build-time constants if you prefer.

Summary

CSS custom properties enable:

  1. Reusable values without a preprocessor
  2. Theme switching with variable overrides
  3. Scoped customization per component
  4. Dynamic changes via JavaScript
  5. Responsive adjustments at breakpoints

Define your design tokens as CSS variables, and your styles become maintainable and themeable.

For tools that work with CSS colors, see the CSS Color Names reference and Gradient Generator on this site.

Frequently Asked Questions

CSS custom properties (also called CSS variables) let you define reusable values with --name syntax and use them with var(). They cascade like other CSS properties and can be overridden per element.

CSS variables work at runtime in the browser, so they can be changed dynamically with JavaScript and respond to context. Sass variables are compiled away at build time.

Define color variables on :root for light mode, then override them in a .dark class or @media (prefers-color-scheme: dark). All elements using those variables automatically update.

Yes. CSS custom properties work with calc(), enabling dynamic calculations like padding: calc(var(--space-unit) * 2).

Use var(--name, fallback) where fallback is used if the variable is not defined. Example: color: var(--accent, blue).

Related Posts