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().
- They behave like named CSS values, not like preprocessor variables.
- They participate in inheritance and the cascade.
- They can store colors, spacing, font sizes, shadows, and even full value lists.
- They are resolved by the browser, which means they can respond to media queries and state changes in CSS.
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
- Theme colors for light and dark mode styles.
- Spacing scales for margins, padding, and gaps.
- Reusable typography settings such as font sizes and line heights.
- Component overrides for cards, modals, buttons, and alerts.
- State-based styling where a parent class changes the values used by children.
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
- Custom properties are resolved at computed-value time, so invalid values can make the entire consuming property fail.
- They are case-sensitive, so --brand-color and --Brand-Color are different names.
- They can store almost any token sequence, but they still must produce a valid value when used in a real property.
- Using them inside shorthand properties can be tricky when the variable expands to an unexpected format.
- Inheritance is powerful, but it can also create surprising overrides when you scope a variable too broadly.
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
- CSS custom properties are user-defined values that begin with --.
- You read them with var(), usually from :root or a component scope.
- They inherit, so scope matters.
- Fallback values help prevent broken styles when a variable is missing.
- They are best for design tokens and reusable style values.
11. Practice Exercise
- Create a small theme for a profile card using CSS variables for background, text color, accent color, and border radius.
- Make a second variant of the card by overriding only the variables, not the entire set of rules.
- Add a fallback color for the accent value so the card still looks good if the variable is removed.
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.