Custom Properties in CSS

Custom properties in CSS, often called CSS variables, let you store reusable values directly in CSS. They are commonly used for colors, spacing, font sizes, shadows, border radius, layout sizes, gradients, animation timing, and theme values. They make CSS easier to maintain because a value can be named once and reused many times.

Custom properties are different from preprocessor variables because they exist in the browser at runtime. They participate in the cascade, inherit by default, can change inside media queries, can be updated by JavaScript, and can be scoped to specific components.

Basic Custom Property Syntax

A custom property starts with two hyphens. It is usually defined on :root for global values, then used with the var() function.

:root {
  --brand-color: #2563eb;
}

.button {
  background: var(--brand-color);
}

The button background uses the value stored in --brand-color. If the value changes, every place using it updates.

The var() Function

The var() function reads a custom property. It can also include a fallback value.

.card {
  border-color: var(--card-border, #e5e7eb);
}

If --card-border is not defined, the fallback color is used. Fallbacks make component CSS safer.

Scope of Custom Properties

Custom properties follow normal CSS scope and cascade rules. A value defined on a component applies to that component and its children.

.alert {
  --accent: #dc2626;
  border-left: 4px solid var(--accent);
}

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

The accent value belongs to the alert component. Links inside the alert can reuse it without creating a global variable.

Inheritance

Custom properties inherit by default. This makes them useful for themes and component variants.

.theme-dark {
  --surface: #111827;
  --text: #f9fafb;
}

.panel {
  background: var(--surface);
  color: var(--text);
}

If the panel is inside .theme-dark, it receives the dark theme values. If it is outside, it can use another theme or fallback values.

Design Tokens

Custom properties are excellent for design tokens. A design token is a named design decision, such as a color, spacing value, radius, or shadow.

:root {
  --space-1: 0.5rem;
  --space-2: 1rem;
  --space-3: 1.5rem;
  --radius-md: 12px;
  --shadow-md: 0 8px 24px rgb(15 23 42 / 0.12);
}

Tokens create consistency. Instead of guessing spacing and shadow values for every component, the system provides reusable choices.

Theming with Custom Properties

Themes can be created by changing custom property values. The component CSS stays the same.

:root {
  --bg: #ffffff;
  --text: #111827;
}

[data-theme="dark"] {
  --bg: #111827;
  --text: #f9fafb;
}

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

Changing the data attribute changes the theme values. Components using those values update automatically.

Custom Properties with calc

Custom properties work inside calc(). This is useful for layout relationships.

:root {
  --sidebar-width: 280px;
  --gap: 2rem;
}

.content {
  width: calc(100% - var(--sidebar-width) - var(--gap));
}

Named values make the calculation easier to understand and update.

Custom Properties with clamp

Fluid design values can also be stored as custom properties.

:root {
  --step-2: clamp(1.75rem, 1.2rem + 2vw, 3rem);
  --section-gap: clamp(2rem, 6vw, 6rem);
}

h2 {
  font-size: var(--step-2);
}

This keeps responsive typography and spacing consistent across the site.

Runtime Updates with JavaScript

Because custom properties exist in the browser, JavaScript can update them.

document.documentElement.style.setProperty("--brand-color", "#ec4899");

This can power theme pickers, live previews, dashboard personalization, and component controls. Use it carefully so styling remains predictable.

Fallback Values

Fallbacks make CSS safer when a variable may not exist.

.button {
  background: var(--button-bg, #2563eb);
  color: var(--button-text, white);
}

If the component consumer does not define custom button colors, the default values still work.

Invalid Values

If a custom property creates an invalid final value, the declaration using it becomes invalid. The browser does not always fail where the variable is defined; it fails where the value is used.

:root {
  --size: red;
}

.box {
  width: var(--size);
}

The variable exists, but red is not a valid width. Debug the final property, not only the variable definition.

@property

The @property rule can register a custom property with syntax, inheritance behavior, and an initial value. This can make custom properties animatable and safer.

@property --angle {
  syntax: "<angle>";
  inherits: false;
  initial-value: 0deg;
}

This is an advanced feature, but it is useful for animations, typed design tokens, and custom interactive effects.

Naming Custom Properties

Names should describe purpose, not only appearance. A variable called --danger is more meaningful than --red if the color represents an error state.

  • Use global tokens for broad design values.
  • Use component variables for component-specific customization.
  • Prefer purpose names for semantic values.
  • Keep token naming consistent.
  • Use fallbacks for reusable components.
  • Avoid creating variables for values used only once.

Component-Level Customization

Custom properties are useful when a component needs safe customization points. The component can expose a few variables while keeping internal structure private.

.button {
  --button-bg: #2563eb;
  --button-text: white;
  --button-radius: 8px;
  background: var(--button-bg);
  color: var(--button-text);
  border-radius: var(--button-radius);
}

.button.danger {
  --button-bg: #dc2626;
}

The danger variant changes only the variable. The base component still owns the actual styling rules. This keeps variants small and predictable.

Custom Properties in Media Queries

Custom properties can change inside media queries. This lets a design token adapt by screen size while components continue using the same variable.

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

@media (min-width: 900px) {
  :root {
    --page-gutter: 3rem;
  }
}

.page {
  padding-inline: var(--page-gutter);
}

The page does not need multiple padding declarations. It simply follows the token value for the current viewport.

Custom Properties and Cascade Layers

Custom properties are still declarations, so they participate in the cascade. A variable defined in a later layer or more specific context can override an earlier value.

@layer theme {
  :root {
    --brand: #2563eb;
  }
}

@layer page {
  .landing-page {
    --brand: #ec4899;
  }
}

Elements inside .landing-page use the page-specific brand value. The cascade controls which variable value is active.

Custom Properties and Fallback Chains

Fallbacks can be nested. This lets a component look for a local value first, then a global value, then a hardcoded fallback.

.card {
  background: var(--card-bg, var(--surface-bg, #ffffff));
}

This is useful for reusable components. A page can define --card-bg, a theme can define --surface-bg, and the component still has a final safe default.

Debugging Custom Properties

When a variable value seems wrong, inspect the element in DevTools and find where the active custom property is defined. Remember that the variable is resolved on the element where it is used, not necessarily where it was first declared.

  • Check spelling of the custom property name.
  • Check whether var() is used correctly.
  • Check the scope where the variable is defined.
  • Check cascade order and specificity.
  • Check fallback values.
  • Check whether the final value is valid for the property.

Semantic vs Raw Tokens

Custom properties can describe raw values or semantic purpose. Raw tokens describe the value itself, such as --blue-600. Semantic tokens describe usage, such as --color-primary or --color-danger.

:root {
  --blue-600: #2563eb;
  --red-600: #dc2626;
  --color-primary: var(--blue-600);
  --color-danger: var(--red-600);
}

Semantic tokens make themes easier because the meaning can stay the same while the actual color changes. Components should usually consume semantic tokens rather than raw palette values.

Local Defaults with Override Hooks

A component can define local defaults and still allow outside customization. This pattern is useful for reusable cards, buttons, alerts, and widgets.

.alert {
  --alert-bg: #eff6ff;
  --alert-text: #1e3a8a;
  background: var(--alert-bg);
  color: var(--alert-text);
}

.alert.warning {
  --alert-bg: #fffbeb;
  --alert-text: #92400e;
}

The component stays stable because the structure does not change. Only the variables change for the variant.

Custom Properties and JavaScript State

JavaScript can update custom properties for interactive effects without rewriting many style rules. For example, a range input can update a progress color, angle, or size variable.

element.style.setProperty("--progress", "64%");

The CSS can then use var(--progress) in a gradient, transform, or width. This keeps dynamic values in one place and lets CSS handle presentation.

When Not to Use Custom Properties

Do not create a custom property for every single value. If a value appears once and has no design meaning, a normal declaration is simpler. Too many variables can make CSS harder to read because developers must jump around to find actual values.

Use custom properties for values that repeat, define a theme, configure a component, or express a meaningful design decision.

Naming Variables for Long-Term Maintenance

The hardest part of custom properties is often naming. A name should explain why the value exists, not only what the current value looks like. For example, --color-primary is usually more useful than --blue because the primary color may become green, red, or orange in another theme.

Use raw palette tokens for the design system and semantic tokens for components. A palette token can store an exact color value, while a semantic token explains purpose. This keeps the system flexible: designers can change the palette without forcing every component selector to be rewritten.

Avoid names that depend on temporary layout details, such as --left-box-color. If the layout changes later, the name becomes misleading even if the CSS still works.

Clear names also help teams review CSS faster. A developer should be able to understand the role of a variable before searching the whole stylesheet for its definition.

This makes variables useful documentation too.

Common Custom Property Mistakes

  • Forgetting to use var() when reading a custom property.
  • Defining too many one-off variables.
  • Using visual names when semantic names would be clearer.
  • Forgetting custom properties inherit.
  • Creating invalid final values.
  • Not providing fallbacks in reusable components.

Custom Properties in CSS FAQ

What are custom properties in CSS?

Custom properties are reusable CSS values defined with names that start with two hyphens.

Are custom properties the same as Sass variables?

No. CSS custom properties exist at runtime and participate in the cascade and inheritance.

How do I use a CSS variable?

Define it with a name like –brand-color, then read it with var(–brand-color).

Do custom properties inherit?

Yes. Custom properties inherit by default unless registered differently with @property.


Continue learning CSS in order
Follow the topic sequence with the previous and next lesson.