On this page
Custom properties (CSS variables)
What are custom properties?
Custom properties (also called CSS variables) are reusable values that you define with -- and consume with var(). Unlike Sass or Less variables, custom properties are browser-native, work at runtime, and participate in the cascade.
:root {
--my-color: #ff5500;
}
.element {
color: var(--my-color);
}Defining variables
Variables are defined inside any CSS selector. The convention is to define global ones in :root:
:root {
--color-primary: oklch(65% 0.2 25);
--spacing: 1rem;
--font-base: system-ui, sans-serif;
}Scope: cascade and inheritance
Custom properties inherit to children, just like color or font-family. This means you can redefine a variable on a container and all its children will use it:
:root { --bg: white; }
.dark-section {
--bg: #1a1a2e; /* Only inside this section */
}
.card {
background: var(--bg); /* Inherits from context */
}Consuming variables with var()
The var() function accepts two arguments: the variable and an optional fallback value:
.element {
color: var(--my-color, #333);
/* If --my-color does not exist, uses #333 */
}Fallbacks can be other var() calls:
color: var(--color-theme, var(--color-base, black));Component variables
A powerful pattern is to define private variables inside a component and override them with modifiers:
.button {
--btn-bg: #1a1a2e;
--btn-text: white;
--btn-radius: 8px;
background: var(--btn-bg);
color: var(--btn-text);
border-radius: var(--btn-radius);
padding: 0.75rem 1.5rem;
}
.button--primary { --btn-bg: #ff530f; }
.button--secondary { --btn-bg: transparent; --btn-text: #1a1a2e; }
.button--pill { --btn-radius: 999px; }Theming with custom properties
The greatest advantage of custom properties is theming. You define tokens once and change them based on context:
:root {
--bg: white;
--text: #1a1a2e;
--border: #e0e0e0;
}
@media (prefers-color-scheme: dark) {
:root {
--bg: #0a0a0f;
--text: #e8e8e8;
--border: rgb(255 255 255 / 10%);
}
}
/* You can also have themes by class */
[data-theme="high-contrast"] {
--bg: black;
--text: white;
--border: white;
}Dynamic variables from HTML
You can pass variables from HTML with inline style. This is useful for values that come from JavaScript or dynamic data:
<div class="progress" style="--value: 75%"></div>.progress::after {
width: var(--value, 0%);
}Variables with calc() and other functions
Custom properties can be used inside CSS functions:
:root {
--base: 1rem;
}
.large {
font-size: calc(var(--base) * 2); /* 2rem */
padding: calc(var(--base) * 0.5); /* 0.5rem */
}Custom properties vs preprocessors
| Feature | Custom properties | Sass/Less |
|---|---|---|
| Execution | Browser (runtime) | Compilation (build) |
| Cascade | Yes | No |
| Dynamic themes | Yes | No (without JS) |
| Media queries | Can be redefined | No |
| JavaScript | Accessible | No |
Custom properties do not completely replace preprocessors (which offer loops, mixins, functions), but for theming and dynamic values they are superior.
@property: typed variables
With @property, you can register a custom property with a type, initial value, and inheritance behavior:
@property --progress {
syntax: "<percentage>";
inherits: false;
initial-value: 0%;
}
.bar {
--progress: 0%;
background: linear-gradient(to right, #b056ff var(--progress), #e0e0e0 0);
transition: --progress 500ms ease; /* Now it can be animated */
}
.bar:hover {
--progress: 100%;
}Without @property, custom properties cannot be animated with transitions because the browser does not know their type.
Custom properties are the foundation of maintainable CSS. In the next lesson, we will explore the most recent CSS features in 2026.
Practice
- Define a token system: Create global variables in
:rootfor colors, spacing, and border-radius. Use those variables in at least 3 different components (button, card, badge). - Create variants with component variables: Build a
.buttoncomponent with internal variables (--btn-bg,--btn-text) and create modifiers (.button--primary,.button--secondary) that only redefine the variables. - Animate a custom property with @property: Register a
--progressvariable with@property, apply it to a linear gradient, and animate it withtransitionon hover.
/* Define global variables */
:root {
--color-brand: oklch(75% 0.18 85);
--color-accent: oklch(55% 0.22 340);
--color-text: oklch(15% 0.01 260);
--color-surface: oklch(98% 0.005 260);
--font-sans: system-ui, sans-serif;
--font-mono: "Fira Code", monospace;
--radius-sm: 4px;
--radius-md: 8px;
--radius-lg: 16px;
--space-xs: 0.25rem;
--space-sm: 0.5rem;
--space-md: 1rem;
--space-lg: 2rem;
--space-xl: 4rem;
}
/* Dark theme: just redefine the variables */
@media (prefers-color-scheme: dark) {
:root {
--color-text: oklch(92% 0.01 260);
--color-surface: oklch(12% 0.01 260);
}
}
/* Usage in components */
.card {
background: var(--color-surface);
color: var(--color-text);
border-radius: var(--radius-lg);
padding: var(--space-lg);
font-family: var(--font-sans);
}
.button {
background: var(--color-brand);
padding: var(--space-sm) var(--space-md);
border-radius: var(--radius-md);
border: none;
cursor: pointer;
}
/* Component variables with local scope */
.badge {
--badge-bg: #e0e0e0;
--badge-text: #333;
--badge-size: 0.75rem;
background: var(--badge-bg);
color: var(--badge-text);
font-size: var(--badge-size);
padding: 0.25em 0.75em;
border-radius: 999px;
display: inline-block;
}
/* Variants: just redefine the variables */
.badge--success {
--badge-bg: oklch(85% 0.15 145);
--badge-text: oklch(25% 0.1 145);
}
.badge--error {
--badge-bg: oklch(85% 0.12 25);
--badge-text: oklch(30% 0.15 25);
}
.badge--large {
--badge-size: 1rem;
}
/* Variables from HTML (inline) */
.progress-bar {
height: 8px;
background: #e0e0e0;
border-radius: 4px;
overflow: hidden;
}
.progress-bar::after {
content: "";
display: block;
height: 100%;
width: var(--progress, 0%);
background: var(--color-brand, #b056ff);
border-radius: 4px;
transition: width 500ms ease;
}
Sign in to track your progress