CSS Custom Properties (Variables): How CSS Variables Work

CSS custom properties let you store reusable values in your stylesheet and reference them with the var() function. They are one of the best ways to build flexible, maintainable styles because they work with the cascade, inheritance, and media-query-driven theming.

Quick answer: Define a custom property with a name that starts with --, then read it with var(--name). They are resolved by the browser at computed-value time, so they inherit and can be overridden like normal CSS values.

Difficulty: Beginner

You'll understand this better if you know: basic CSS selectors, how the cascade works, and simple property-value declarations.

1. What Are CSS Custom Properties?

CSS custom properties are user-defined values that begin with --. You declare them on any selector and reuse them elsewhere in your CSS with var().

Unlike Sass variables, custom properties remain available in the final stylesheet that the browser reads, so they can change at runtime based on scope and cascade rules.

2. Why CSS Variables Matter

Custom properties make stylesheets easier to maintain. Instead of repeating the same color or spacing value in many places, you define it once and reuse it everywhere.

They are especially useful when you want consistent design tokens, theme switching, responsive adjustments, or component-level overrides. If your site has multiple buttons, cards, and headings that share the same palette, CSS variables let you change the palette in one place.

They also reduce the risk of inconsistent styling. When a value changes, you update the variable instead of searching through many selectors.

3. Basic Syntax or Core Idea

A custom property is declared like any other CSS property, except its name starts with --. You usually define shared values on :root so they are available across the page.

Defining a variable

This example stores a brand color and a spacing value, then uses both in a rule.

:root {
  --brand-color: #2563eb;
  --space-md: 1rem;
}

button {
  background-color: var(--brand-color);
  padding: var(--space-md);
}

The value after the colon is the stored custom property value, and var(--brand-color) inserts that value wherever it is used.

Using a fallback value

You can provide a fallback in case the variable is missing or invalid.

p {
  color: var(--text-color, #222);
}

If --text-color is not defined, the browser uses #222 instead.

4. Step-by-Step Examples

Example 1: Global color palette

Start by defining a small set of design tokens on :root. That makes them available everywhere in the document.

:root {
  --bg: #ffffff;
  --text: #1f2937;
  --accent: #7c3aed;
}

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

a {
  color: var(--accent);
}

This approach keeps your color system centralized and easy to update.

Example 2: Component-level overrides

You can override a variable inside a component scope without affecting the rest of the page.

:root {
  --card-bg: #f9fafb;
}

.card {
  background: var(--card-bg);
  padding: 1.5rem;
}

.card.featured {
  --card-bg: #ecfeff;
}

Because the featured card overrides only its own scope, other cards still use the root value.

Example 3: Responsive values with media queries

Custom properties can change inside media queries, which makes them useful for responsive tokens.

:root {
  --page-gap: 1rem;
}

main {
  padding: var(--page-gap);
}

@media (min-width: 48rem) {
  :root {
    --page-gap: 2rem;
  }
}

The layout gets more breathing room on larger screens without changing every selector that uses the spacing token.

Example 4: Reusing a complex value

Variables are not limited to single words or numbers. They can store full value lists such as shadows or gradients.

:root {
  --panel-shadow: 0 12px 30px rgb(0 0 0 / 0.12);
}

.panel {
  box-shadow: var(--panel-shadow);
}

This is handy when one visual style must stay identical across many components.

5. Practical Use Cases

For example, a marketing site might store brand colors in variables, while a dashboard might use them for compact and comfortable density modes.

6. Common Mistakes

Mistake 1: Forgetting the -- prefix

Custom property names must begin with --. Without that prefix, the browser treats the declaration as invalid.

Problem: The declaration below is not a valid custom property, so var(--brand) has nothing to read.

:root {
  brand-color: #0ea5e9;
}

h1 {
  color: var(--brand-color);
}

Fix: Add the double-hyphen prefix to the declaration name.

:root {
  --brand-color: #0ea5e9;
}

h1 {
  color: var(--brand-color);
}

The corrected version works because the browser recognizes --brand-color as a custom property.

Mistake 2: Using a variable before defining it

If a variable is missing and you do not provide a fallback, the entire property may become invalid.

Problem: Here, var(--spacing) has no value to resolve to, so margin becomes invalid and the browser ignores it.

.box {
  margin: var(--spacing);
}

Fix: Define the variable or give var() a fallback value.

:root {
  --spacing: 1rem;
}

.box {
  margin: var(--spacing, 1rem);
}

The corrected version works because the property always has a usable value.

Mistake 3: Forgetting that custom properties inherit

Variables follow the same inheritance rules as normal CSS, so a child can pick up a value from a parent unexpectedly.

Problem: The button inside .danger-panel inherits the red accent color, even if the button was meant to look neutral.

.panel {
  --accent: #2563eb;
}

.danger-panel {
  --accent: #dc2626;
}

button {
  background: var(--accent);
}

Fix: Override the variable only where the new value is intended, or define a separate variable for button styling.

.panel {
  --panel-accent: #2563eb;
}

.danger-panel {
  --panel-accent: #dc2626;
}

button {
  background: var(--panel-accent);
}

The corrected version is clearer because the variable name reflects the scope where it is meant to vary.

7. Best Practices

Practice 1: Store shared design tokens on :root

Put values that should be available site-wide in the root scope. That makes the system easier to discover and override.

:root {
  --font-base: 1rem;
  --radius-md: 0.5rem;
}

Keeping core tokens together reduces duplication and makes global updates straightforward.

Practice 2: Use descriptive names, not visual guesses

Name variables after their purpose, not just their current color. A token like --surface is more flexible than --gray-2 if the design later changes.

:root {
  --surface: #f8fafc;
  --surface-text: #111827;
}

Meaningful names help future you understand whether a value is tied to color, role, or component state.

Practice 3: Always provide a fallback for optional values

If a variable may be missing in some contexts, supply a fallback so the layout does not break.

.badge {
  background: var(--badge-bg, #e5e7eb);
}

This is especially useful for reusable components and for styles that need to survive partial overrides.

8. Limitations and Edge Cases

A common “not working” case is a property silently falling back to its initial value because the resolved variable value does not fit the property’s syntax. In that situation, check both the variable definition and the property that consumes it.

9. Practical Mini Project

In this mini project, we will build a simple themed card layout using CSS variables for colors, spacing, and radius values. The goal is to show how variables make the design easy to adjust in one place.

:root {
  --page-bg: #f3f4f6;
  --card-bg: #ffffff;
  --text: #111827;
  --muted: #6b7280;
  --accent: #2563eb;
  --space: 1rem;
  --radius: 0.75rem;
}

body {
  margin: 0;
  font-family: system-ui, sans-serif;
  background: var(--page-bg);
  color: var(--text);
}

.card {
  max-width: 28rem;
  margin: 2rem auto;
  padding: var(--space);
  background: var(--card-bg);
  border-radius: var(--radius);
  box-shadow: 0 8px 24px rgb(0 0 0 / 0.08);
}

.card h2 {
  margin-top: 0;
}

.card p {
  color: var(--muted);
}

.card a {
  color: var(--accent);
}

This example shows a small, realistic token system. If the brand color or spacing changes later, you update the variables once and every card follows the new design.

10. Key Points

11. Practice Exercise

Expected output: One card component should use the default theme, and a second card should look visually different because its custom properties are overridden.

Hint: Define all shared values on :root, then override the component-specific values on a class like .profile-card.alt.

:root {
  --card-bg: #ffffff;
  --card-text: #111827;
  --card-accent: #2563eb;
  --card-radius: 1rem;
}

.profile-card {
  background: var(--card-bg);
  color: var(--card-text);
  border-radius: var(--card-radius);
  padding: 1.25rem;
  border: 1px solid var(--card-accent, #9ca3af);
}

.profile-card.alt {
  --card-bg: #0f172a;
  --card-text: #e2e8f0;
  --card-accent: #38bdf8;
}

This solution works because the second card changes only its scoped variables, which automatically updates every property that references them.

12. Final Summary

CSS custom properties are one of the most practical features in modern CSS. They help you centralize repeated values, create themes, and make components easier to maintain without introducing a preprocessing step.

Once you understand that variables are resolved through the cascade and inheritance, they become much easier to reason about. The most important habits are to define shared tokens clearly, scope overrides carefully, and provide fallbacks where needed.

If you want to go further, the next step is learning how CSS custom properties work with media queries, dark mode, and component-based design systems.