DevToolBoxGRATIS
Blog

Variables CSS (Propiedades personalizadas): Guía completa

10 min de lecturapor DevToolBox

CSS Variables, officially known as CSS Custom Properties, have fundamentally changed how we write and maintain stylesheets. They bring the power of variables — previously only available through preprocessors like Sass — directly into the browser, with the added benefits of cascade, inheritance, and runtime manipulation via JavaScript. This complete CSS custom properties guide covers everything from basic syntax and scoping rules to dynamic theming, color systems, animation with @property, and real-world component patterns.

Explore colors visually with our Color Converter tool →

1. Syntax: Declaring and Using CSS Variables

CSS custom properties follow a simple two-part syntax: you declare them with a double-hyphen prefix (--) and consume them with the var() function. The var() function also supports an optional fallback value that is used when the property is not defined.

Declaring Custom Properties

/* Declaring CSS custom properties */
:root {
  --color-primary: #3b82f6;
  --color-secondary: #8b5cf6;
  --font-size-base: 16px;
  --spacing-unit: 8px;
  --border-radius: 8px;
  --font-family: 'Inter', system-ui, sans-serif;
  --transition-speed: 200ms;
  --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
}

/* Using them in rules */
.button {
  background-color: var(--color-primary);
  font-size: var(--font-size-base);
  padding: var(--spacing-unit) calc(var(--spacing-unit) * 2);
  border-radius: var(--border-radius);
  font-family: var(--font-family);
  transition: background-color var(--transition-speed) ease;
  box-shadow: var(--shadow-sm);
}

Using var() with Fallbacks

/* var() with fallback value */
.element {
  /* If --accent is not defined, use #e11d48 */
  color: var(--accent, #e11d48);

  /* Fallback can be another variable */
  background: var(--bg-card, var(--bg-primary, #ffffff));

  /* Fallback with full value including commas */
  font-family: var(--font-stack, 'Helvetica Neue', Arial, sans-serif);
}

/* Common pattern: fallback to a default theme */
.card {
  background: var(--card-bg, var(--surface-color, #1e293b));
  color: var(--card-text, var(--text-primary, #f8fafc));
  padding: var(--card-padding, 24px);
}

Nested Fallbacks and Composition

/* Composing variables with calc() */
:root {
  --base: 8px;
  --scale: 1.5;
}

.component {
  /* Math with variables */
  padding: calc(var(--base) * 2);           /* 16px */
  margin-bottom: calc(var(--base) * 3);     /* 24px */
  gap: calc(var(--base) * var(--scale));     /* 12px */
}

/* String-like composition (for building values) */
:root {
  --h: 220;
  --s: 90%;
  --l: 56%;
}

.element {
  /* Build a full HSL color from parts */
  color: hsl(var(--h), var(--s), var(--l));
  /* Darken by adjusting lightness */
  border-color: hsl(var(--h), var(--s), calc(var(--l) - 10%));
}

2. Scope & Cascade

Unlike Sass variables that are resolved at compile time, CSS custom properties participate in the cascade. They are inherited by default, can be scoped to any selector, and can be overridden at any level of the DOM tree.

Global Variables with :root

/* :root = <html> element — highest specificity for globals */
:root {
  --color-brand: #3b82f6;
  --color-danger: #ef4444;
  --color-success: #22c55e;
  --color-warning: #f59e0b;

  --text-primary: #f8fafc;
  --text-secondary: #94a3b8;

  --bg-primary: #0f172a;
  --bg-secondary: #1e293b;
  --bg-card: #1e293b;
}

/* Every element in the page can now use these */
body {
  color: var(--text-primary);
  background: var(--bg-primary);
}

Component-Level Scope

/* Variables scoped to a component */
.alert {
  --alert-bg: #1e293b;
  --alert-border: #334155;
  --alert-text: #e2e8f0;
  --alert-icon-size: 20px;

  background: var(--alert-bg);
  border: 1px solid var(--alert-border);
  color: var(--alert-text);
  padding: 16px;
  border-radius: 8px;
}

/* Variants override the component-scoped variables */
.alert--danger {
  --alert-bg: #450a0a;
  --alert-border: #ef4444;
  --alert-text: #fecaca;
}

.alert--success {
  --alert-bg: #052e16;
  --alert-border: #22c55e;
  --alert-text: #bbf7d0;
}

.alert--warning {
  --alert-bg: #451a03;
  --alert-border: #f59e0b;
  --alert-text: #fef3c7;
}

Inheritance and Override

Custom properties inherit down the DOM tree just like color or font-size. A child element can read a variable set on any ancestor, and any element can override a variable for its subtree.

/* Parent sets the variable */
.sidebar {
  --link-color: #60a5fa;
}

/* Child inherits it */
.sidebar a {
  color: var(--link-color);  /* #60a5fa */
}

/* Grandchild can override */
.sidebar .promo-section {
  --link-color: #f59e0b;     /* override for this subtree */
}

.sidebar .promo-section a {
  color: var(--link-color);  /* #f59e0b */
}

/* Variables NOT inherited by siblings or parents */
.main-content a {
  /* This will NOT be #60a5fa — falls back to default */
  color: var(--link-color, #3b82f6);
}

3. Dynamic Theming

One of the most powerful applications of CSS variables is dynamic theming. By changing a few variable values, you can transform the entire look of your site — no class toggling on individual elements needed.

Dark Mode Toggle with CSS Variables

/* Light theme (default) */
:root {
  --bg-primary: #ffffff;
  --bg-secondary: #f1f5f9;
  --bg-card: #ffffff;
  --text-primary: #0f172a;
  --text-secondary: #64748b;
  --border-color: #e2e8f0;
  --shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}

/* Dark theme — toggled via class on <html> or <body> */
:root.dark {
  --bg-primary: #0f172a;
  --bg-secondary: #1e293b;
  --bg-card: #1e293b;
  --text-primary: #f8fafc;
  --text-secondary: #94a3b8;
  --border-color: #334155;
  --shadow: 0 1px 3px rgba(0, 0, 0, 0.4);
}

/* All components use the variables — theme changes automatically */
body { background: var(--bg-primary); color: var(--text-primary); }
.card { background: var(--bg-card); border: 1px solid var(--border-color); box-shadow: var(--shadow); }
.text-muted { color: var(--text-secondary); }
// Toggle dark mode with JavaScript
const toggle = document.getElementById('theme-toggle');
toggle.addEventListener('click', () => {
  document.documentElement.classList.toggle('dark');
  // Persist preference
  const isDark = document.documentElement.classList.contains('dark');
  localStorage.setItem('theme', isDark ? 'dark' : 'light');
});

// Apply saved preference on load
if (localStorage.getItem('theme') === 'dark') {
  document.documentElement.classList.add('dark');
}

prefers-color-scheme Media Query

/* Auto-detect OS/browser dark mode preference */
@media (prefers-color-scheme: dark) {
  :root {
    --bg-primary: #0f172a;
    --bg-secondary: #1e293b;
    --bg-card: #1e293b;
    --text-primary: #f8fafc;
    --text-secondary: #94a3b8;
    --border-color: #334155;
  }
}

/* Combine with manual override */
@media (prefers-color-scheme: dark) {
  :root:not(.light) {
    --bg-primary: #0f172a;
    --text-primary: #f8fafc;
    /* ... dark values */
  }
}

@media (prefers-color-scheme: light) {
  :root:not(.dark) {
    --bg-primary: #ffffff;
    --text-primary: #0f172a;
    /* ... light values */
  }
}

Multi-Theme Switching

/* Define multiple themes using data attributes */
:root,
[data-theme="default"] {
  --brand: #3b82f6;
  --brand-light: #93c5fd;
  --accent: #8b5cf6;
}

[data-theme="sunset"] {
  --brand: #f97316;
  --brand-light: #fdba74;
  --accent: #ef4444;
}

[data-theme="forest"] {
  --brand: #22c55e;
  --brand-light: #86efac;
  --accent: #14b8a6;
}

[data-theme="ocean"] {
  --brand: #0ea5e9;
  --brand-light: #7dd3fc;
  --accent: #6366f1;
}
// Switch themes from JavaScript
function setTheme(themeName) {
  document.documentElement.setAttribute('data-theme', themeName);
  localStorage.setItem('theme', themeName);
}

// Usage
setTheme('sunset');
setTheme('forest');
setTheme('default');

4. Color Systems with CSS Variables

CSS variables excel at building systematic color palettes. The HSL color model is particularly powerful because you can manipulate hue, saturation, and lightness independently.

HSL Approach for Color Palettes

/* HSL-based color palette — change one hue to update entire palette */
:root {
  /* Primary color — change --primary-h to shift the whole palette */
  --primary-h: 220;
  --primary-s: 90%;

  --primary-50:  hsl(var(--primary-h), var(--primary-s), 97%);
  --primary-100: hsl(var(--primary-h), var(--primary-s), 93%);
  --primary-200: hsl(var(--primary-h), var(--primary-s), 83%);
  --primary-300: hsl(var(--primary-h), var(--primary-s), 73%);
  --primary-400: hsl(var(--primary-h), var(--primary-s), 63%);
  --primary-500: hsl(var(--primary-h), var(--primary-s), 53%);  /* base */
  --primary-600: hsl(var(--primary-h), var(--primary-s), 43%);
  --primary-700: hsl(var(--primary-h), var(--primary-s), 33%);
  --primary-800: hsl(var(--primary-h), var(--primary-s), 23%);
  --primary-900: hsl(var(--primary-h), var(--primary-s), 13%);

  /* Secondary — just change hue */
  --secondary-h: 270;
  --secondary-s: 80%;
  --secondary-500: hsl(var(--secondary-h), var(--secondary-s), 53%);
}

/* To create a completely different color scheme, change one value: */
:root.warm {
  --primary-h: 15;  /* orange */
}

Semantic Color Tokens

/* Map raw colors to semantic tokens */
:root {
  /* Raw palette (primitive tokens) */
  --blue-500: #3b82f6;
  --blue-600: #2563eb;
  --red-500: #ef4444;
  --green-500: #22c55e;
  --gray-100: #f1f5f9;
  --gray-800: #1e293b;
  --gray-900: #0f172a;

  /* Semantic tokens (reference tokens) */
  --color-action: var(--blue-500);
  --color-action-hover: var(--blue-600);
  --color-error: var(--red-500);
  --color-success: var(--green-500);
  --color-bg: var(--gray-900);
  --color-surface: var(--gray-800);
  --color-text: var(--gray-100);
}

/* Components use semantic tokens, never raw colors */
.btn-primary {
  background: var(--color-action);
}
.btn-primary:hover {
  background: var(--color-action-hover);
}
.error-message {
  color: var(--color-error);
}

Alpha / Opacity Variations

/* Modern approach: store components, compose in usage */
:root {
  --brand-rgb: 59, 130, 246;
  --danger-rgb: 239, 68, 68;
}

.overlay {
  background: rgba(var(--brand-rgb), 0.1);   /* 10% opacity */
}
.overlay-hover:hover {
  background: rgba(var(--brand-rgb), 0.2);   /* 20% opacity */
}
.border-subtle {
  border: 1px solid rgba(var(--brand-rgb), 0.3);
}

/* Modern CSS: using color-mix() (no RGB splitting needed) */
:root {
  --brand: #3b82f6;
}

.element {
  /* 20% brand color mixed with transparent */
  background: color-mix(in srgb, var(--brand) 20%, transparent);
  /* 50% brand color */
  border-color: color-mix(in srgb, var(--brand) 50%, transparent);
}

5. Spacing & Typography Systems

A consistent spacing scale and a fluid typography system are the backbone of any well-designed interface. CSS variables make both easy to define, maintain, and override.

Spacing Scale

/* 8px-based spacing scale */
:root {
  --space-1:  4px;    /* 0.25rem */
  --space-2:  8px;    /* 0.5rem  */
  --space-3:  12px;   /* 0.75rem */
  --space-4:  16px;   /* 1rem    */
  --space-5:  20px;   /* 1.25rem */
  --space-6:  24px;   /* 1.5rem  */
  --space-8:  32px;   /* 2rem    */
  --space-10: 40px;   /* 2.5rem  */
  --space-12: 48px;   /* 3rem    */
  --space-16: 64px;   /* 4rem    */
  --space-20: 80px;   /* 5rem    */
}

/* Usage: consistent spacing everywhere */
.card {
  padding: var(--space-6);
  gap: var(--space-4);
}

.section {
  padding-block: var(--space-16);
}

.stack > * + * {
  margin-top: var(--space-4);
}

Fluid Typography with clamp()

/* Fluid typography: scales between viewport widths */
:root {
  /* clamp(min, preferred, max) */
  --text-sm:  clamp(0.8rem,  0.17vw + 0.76rem,  0.89rem);
  --text-base: clamp(1rem,    0.34vw + 0.91rem,  1.19rem);
  --text-lg:  clamp(1.25rem, 0.61vw + 1.1rem,   1.58rem);
  --text-xl:  clamp(1.56rem, 1vw + 1.31rem,     2.11rem);
  --text-2xl: clamp(1.95rem, 1.56vw + 1.56rem,  2.81rem);
  --text-3xl: clamp(2.44rem, 2.38vw + 1.85rem,  3.75rem);
}

h1 { font-size: var(--text-3xl); }
h2 { font-size: var(--text-2xl); }
h3 { font-size: var(--text-xl); }
p  { font-size: var(--text-base); }
small { font-size: var(--text-sm); }

Type Scale System

/* Modular type scale using calc() */
:root {
  --type-base: 1rem;
  --type-ratio: 1.25;  /* Major third scale */

  --type-sm:  calc(var(--type-base) / var(--type-ratio));
  --type-md:  var(--type-base);
  --type-lg:  calc(var(--type-base) * var(--type-ratio));
  --type-xl:  calc(var(--type-base) * var(--type-ratio) * var(--type-ratio));
  --type-2xl: calc(var(--type-base) * var(--type-ratio) * var(--type-ratio) * var(--type-ratio));

  --leading-tight: 1.2;
  --leading-normal: 1.5;
  --leading-relaxed: 1.75;
}

/* Responsive: change base and ratio, everything scales */
@media (min-width: 1024px) {
  :root {
    --type-base: 1.125rem;
    --type-ratio: 1.333;  /* Perfect fourth on large screens */
  }
}

6. CSS Variables vs Sass Variables

Both CSS custom properties and Sass variables serve the purpose of reducing repetition, but they work in fundamentally different ways. The choice is not either/or — many projects use both.

Comparison Table

FeatureCSS Custom PropertiesSass Variables
ResolutionRuntime (browser)Compile-time (build)
CascadeYes — inherits, overridableNo — flat scope
JS AccessYes — read/write from JSNo — compiled away
Media QueriesYes — change in @mediaCompile-time only
Functionscalc() onlyRich functions, loops, mixins
Browser Support97%+ (all modern)Compiled to CSS — universal
PerformanceTiny runtime costZero runtime cost

When to Use Which

Use CSS variables for anything that changes at runtime: theming, responsive adjustments, user preferences, component API surfaces. Use Sass variables for static design tokens, complex calculations during build, conditional compilation, and generating utility classes. Best practice: Define design tokens as Sass variables, then expose them as CSS custom properties for runtime flexibility.

/* Best practice: Sass variables → CSS custom properties */

/* _tokens.scss (Sass source) */
$color-primary: #3b82f6;
$color-secondary: #8b5cf6;
$spacing-base: 8px;
$font-stack: 'Inter', system-ui, sans-serif;
$breakpoint-md: 768px;

/* Expose as CSS custom properties */
:root {
  --color-primary: #{$color-primary};
  --color-secondary: #{$color-secondary};
  --spacing-base: #{$spacing-base};
  --font-stack: #{$font-stack};
}

/* Use Sass for build-time logic */
@for $i from 1 through 8 {
  .space-#{$i} {
    --space: calc(var(--spacing-base) * #{$i});
    margin: var(--space);
  }
}

/* Use CSS variables for runtime values */
.component {
  color: var(--color-primary);
  font-family: var(--font-stack);
}

7. JavaScript Interaction

CSS custom properties can be read and written from JavaScript, making them a powerful bridge between CSS and JS. This enables dynamic styling without managing inline styles on every element.

Reading CSS Variables from JS

// Read a CSS variable from :root
const root = document.documentElement;
const styles = getComputedStyle(root);

const primaryColor = styles.getPropertyValue('--color-primary').trim();
console.log(primaryColor); // "#3b82f6"

// Read from a specific element
const card = document.querySelector('.card');
const cardBg = getComputedStyle(card).getPropertyValue('--card-bg').trim();

// Read all CSS variables on an element
function getCSSVariables(element) {
  const styles = getComputedStyle(element);
  const vars = {};
  for (const prop of styles) {
    if (prop.startsWith('--')) {
      vars[prop] = styles.getPropertyValue(prop).trim();
    }
  }
  return vars;
}

Writing CSS Variables from JS

// Set a CSS variable on :root (affects entire page)
document.documentElement.style.setProperty('--color-primary', '#e11d48');

// Set on a specific element (affects element + descendants)
const sidebar = document.querySelector('.sidebar');
sidebar.style.setProperty('--sidebar-width', '300px');

// Remove a custom property (revert to inherited/default)
document.documentElement.style.removeProperty('--color-primary');

// Dynamic theming based on user input
const colorPicker = document.getElementById('brand-color');
colorPicker.addEventListener('input', (e) => {
  document.documentElement.style.setProperty('--brand', e.target.value);
});

// Set multiple variables at once
function applyTheme(theme) {
  const root = document.documentElement.style;
  Object.entries(theme).forEach(([key, value]) => {
    root.setProperty(key, value);
  });
}

applyTheme({
  '--bg-primary': '#0f172a',
  '--text-primary': '#f8fafc',
  '--brand': '#3b82f6',
});

Reactive Patterns

// React: CSS variables as component API
function Card({ accentColor, padding, children }) {
  return (
    <div
      className="card"
      style={{
        '--card-accent': accentColor,
        '--card-padding': padding,
      }}
    >
      {children}
    </div>
  );
}

// Vue 3: v-bind in <style>
// <style scoped>
// .element {
//   color: v-bind('themeColor');  /* compiles to CSS variable */
// }
// </style>

// Vanilla JS: Respond to scroll position
window.addEventListener('scroll', () => {
  const scrollPercent = window.scrollY / (document.body.scrollHeight - window.innerHeight);
  document.documentElement.style.setProperty('--scroll', scrollPercent);
});

// CSS uses the variable
// .progress-bar { transform: scaleX(var(--scroll)); }
// .parallax     { transform: translateY(calc(var(--scroll) * -50px)); }

8. Animating with CSS Variables

By default, CSS custom properties cannot be animated because the browser does not know what type of value they hold. The @property rule solves this by registering a property with a specific syntax.

The Problem: Why Variables Don't Animate

/* This does NOT work — browser treats variables as strings */
:root {
  --box-width: 100px;
}

.box {
  width: var(--box-width);
  transition: width 0.3s ease;  /* ← will NOT animate */
}

.box:hover {
  --box-width: 300px;  /* Jumps instantly, no transition */
}

/* Why? The browser sees: transition on 'width', but the value
   change happens to '--box-width' (a string). The browser
   doesn't know --box-width contains a <length>. */

The Solution: @property Registration

/* Register the property with @property — now it animates! */
@property --box-width {
  syntax: '<length>';
  initial-value: 100px;
  inherits: false;
}

.box {
  width: var(--box-width);
  transition: --box-width 0.3s ease;  /* transition the PROPERTY itself */
}

.box:hover {
  --box-width: 300px;  /* Now smoothly animates! */
}

/* Animated color shift */
@property --glow-color {
  syntax: '<color>';
  initial-value: #3b82f6;
  inherits: false;
}

.glow-button {
  box-shadow: 0 0 20px var(--glow-color);
  transition: --glow-color 0.5s ease;
}

.glow-button:hover {
  --glow-color: #ec4899;  /* Smooth color transition in shadow */
}

Animated Gradient Background

/* Animate gradient positions with @property */
@property --gradient-angle {
  syntax: '<angle>';
  initial-value: 0deg;
  inherits: false;
}

@property --color-1 {
  syntax: '<color>';
  initial-value: #3b82f6;
  inherits: false;
}

@property --color-2 {
  syntax: '<color>';
  initial-value: #8b5cf6;
  inherits: false;
}

.animated-gradient {
  background: linear-gradient(
    var(--gradient-angle),
    var(--color-1),
    var(--color-2)
  );
  animation: gradient-rotate 4s linear infinite;
}

@keyframes gradient-rotate {
  0%   { --gradient-angle: 0deg;   --color-1: #3b82f6; --color-2: #8b5cf6; }
  33%  { --gradient-angle: 120deg; --color-1: #ec4899; --color-2: #f59e0b; }
  66%  { --gradient-angle: 240deg; --color-1: #22c55e; --color-2: #06b6d4; }
  100% { --gradient-angle: 360deg; --color-1: #3b82f6; --color-2: #8b5cf6; }
}

9. The @property Rule

The @property at-rule is part of the CSS Houdini specification. It lets you formally register a custom property with a type, initial value, and inheritance behavior. This unlocks animation and validation.

Full Syntax

@property --property-name {
  syntax: '<type>';        /* Required: what type of value this holds */
  initial-value: value;    /* Required (if inherits: false): default value */
  inherits: true | false;  /* Required: does it inherit down the DOM? */
}

/* Examples */
@property --my-color {
  syntax: '<color>';
  initial-value: #3b82f6;
  inherits: true;
}

@property --progress {
  syntax: '<percentage>';
  initial-value: 0%;
  inherits: false;
}

@property --columns {
  syntax: '<integer>';
  initial-value: 3;
  inherits: false;
}

Supported Types

/* All supported syntax types for @property */

'<length>'           /* 10px, 2rem, 50vh */
'<number>'           /* 1.5, 42, 0.8 */
'<percentage>'       /* 50%, 100% */
'<length-percentage>'/* 10px or 50% */
'<color>'            /* #fff, rgb(), hsl(), named colors */
'<image>'            /* url(), gradient functions */
'<url>'              /* url(...) */
'<integer>'          /* 1, 2, 3 (whole numbers) */
'<angle>'            /* 45deg, 0.5turn, 100grad */
'<time>'             /* 200ms, 1.5s */
'<resolution>'       /* 96dpi, 2dppx */
'<transform-function>'  /* rotate(), scale(), translate() */
'<transform-list>'   /* multiple transforms */
'<custom-ident>'     /* any identifier */
'*'                  /* any value (universal) */

/* Combining types */
'<length> | <percentage>'  /* either type */
'<color>+'                 /* space-separated list of colors */
'<length>#'                /* comma-separated list of lengths */

Practical Typed Properties

/* Circular progress indicator */
@property --progress {
  syntax: '<percentage>';
  initial-value: 0%;
  inherits: false;
}

.circular-progress {
  --progress: 0%;
  background: conic-gradient(
    var(--color-primary) var(--progress),
    var(--bg-secondary) var(--progress)
  );
  border-radius: 50%;
  width: 100px;
  height: 100px;
  transition: --progress 1s ease-out;
}

.circular-progress.loaded {
  --progress: 75%;
}

/* Counting animation */
@property --num {
  syntax: '<integer>';
  initial-value: 0;
  inherits: false;
}

.counter {
  animation: count-up 2s ease-out forwards;
  counter-reset: num var(--num);
}

.counter::after {
  content: counter(num);
}

@keyframes count-up {
  to { --num: 1000; }
}

10. Component Patterns

CSS variables create a clean API for components. Instead of overriding internal selectors, consumers set variables from the outside. This pattern works with any framework or plain HTML.

Button Component

/* Button with CSS variable API */
.btn {
  /* Internal defaults — consumers override these */
  --btn-bg: #3b82f6;
  --btn-color: #ffffff;
  --btn-border: transparent;
  --btn-radius: 8px;
  --btn-padding-x: 20px;
  --btn-padding-y: 10px;
  --btn-font-size: 0.875rem;
  --btn-hover-bg: color-mix(in srgb, var(--btn-bg), black 15%);

  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: var(--btn-padding-y) var(--btn-padding-x);
  background: var(--btn-bg);
  color: var(--btn-color);
  border: 2px solid var(--btn-border);
  border-radius: var(--btn-radius);
  font-size: var(--btn-font-size);
  cursor: pointer;
  transition: background 200ms ease;
}

.btn:hover {
  background: var(--btn-hover-bg);
}

/* Variants just override variables */
.btn-secondary {
  --btn-bg: transparent;
  --btn-color: #3b82f6;
  --btn-border: #3b82f6;
  --btn-hover-bg: rgba(59, 130, 246, 0.1);
}

.btn-danger {
  --btn-bg: #ef4444;
  --btn-hover-bg: #dc2626;
}

.btn-lg {
  --btn-padding-x: 32px;
  --btn-padding-y: 14px;
  --btn-font-size: 1rem;
}

Card Component

/* Card component with variable API */
.card {
  --card-bg: var(--bg-card, #1e293b);
  --card-border: var(--border-color, #334155);
  --card-radius: 12px;
  --card-padding: 24px;
  --card-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
  --card-hover-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.2);

  background: var(--card-bg);
  border: 1px solid var(--card-border);
  border-radius: var(--card-radius);
  padding: var(--card-padding);
  box-shadow: var(--card-shadow);
  transition: box-shadow 200ms ease, transform 200ms ease;
}

.card:hover {
  box-shadow: var(--card-hover-shadow);
  transform: translateY(-2px);
}

.card-compact {
  --card-padding: 16px;
  --card-radius: 8px;
}

.card-featured {
  --card-border: var(--color-primary, #3b82f6);
  --card-shadow: 0 4px 6px -1px rgba(59, 130, 246, 0.15);
  --card-hover-shadow: 0 10px 15px -3px rgba(59, 130, 246, 0.25);
}

Form Input Component

/* Form input with variable API */
.input {
  --input-bg: var(--bg-secondary, #1e293b);
  --input-border: var(--border-color, #334155);
  --input-text: var(--text-primary, #f8fafc);
  --input-placeholder: var(--text-secondary, #64748b);
  --input-focus-border: var(--color-primary, #3b82f6);
  --input-radius: 8px;
  --input-padding: 10px 14px;
  --input-font-size: 0.875rem;

  width: 100%;
  padding: var(--input-padding);
  background: var(--input-bg);
  border: 2px solid var(--input-border);
  border-radius: var(--input-radius);
  color: var(--input-text);
  font-size: var(--input-font-size);
  outline: none;
  transition: border-color 200ms ease, box-shadow 200ms ease;
}

.input::placeholder {
  color: var(--input-placeholder);
}

.input:focus {
  border-color: var(--input-focus-border);
  box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.15);
}

/* Error state */
.input-error {
  --input-border: var(--color-error, #ef4444);
  --input-focus-border: var(--color-error, #ef4444);
}

/* Success state */
.input-success {
  --input-border: var(--color-success, #22c55e);
  --input-focus-border: var(--color-success, #22c55e);
}

11. Browser Support & Fallbacks

CSS custom properties are supported in all modern browsers. However, if you need to support legacy browsers, there are several fallback strategies.

Current Browser Support

CSS custom properties have 97%+ global support as of 2025. They work in Chrome 49+, Firefox 31+, Safari 9.1+, Edge 15+, and all Chromium-based browsers. The @property rule has more limited support (Chrome 85+, Safari 15.4+, Firefox 128+).

Fallback Strategies

/* Strategy 1: Declare fallback value before the variable usage */
.element {
  /* Old browsers use this */
  color: #3b82f6;
  /* Modern browsers override with the variable */
  color: var(--color-primary, #3b82f6);
}

/* Strategy 2: Cascade fallback */
.card {
  background: #1e293b;                  /* fallback for IE */
  background: var(--bg-card, #1e293b);  /* modern browsers */
  border: 1px solid #334155;
  border: 1px solid var(--border-color, #334155);
}

/* Strategy 3: PostCSS plugin (build-time) */
/* postcss-custom-properties compiles var() to static values */
/* Input: */  .box { color: var(--brand); }
/* Output: */ .box { color: #3b82f6; color: var(--brand); }

Using @supports

/* Feature detection with @supports */
@supports (--css: variables) {
  /* CSS variables are supported */
  :root {
    --color-primary: #3b82f6;
  }

  .element {
    color: var(--color-primary);
  }
}

@supports not (--css: variables) {
  /* Fallback for browsers without CSS variable support */
  .element {
    color: #3b82f6;
  }
}

/* JavaScript feature detection */
// if (window.CSS && window.CSS.supports &&
//     window.CSS.supports('--test', '0')) {
//   // CSS variables supported
// }

/* @property feature detection */
@supports (syntax: '<color>') {
  @property --animated-color {
    syntax: '<color>';
    initial-value: #3b82f6;
    inherits: false;
  }
}

12. Frequently Asked Questions

What is the difference between CSS variables and CSS custom properties?

They are the same thing. "CSS Custom Properties" is the official W3C specification name, while "CSS Variables" is the commonly used informal name. The formal specification is called "CSS Custom Properties for Cascading Variables." When people say "CSS variables," they mean properties declared with the -- prefix and accessed via the var() function.

Can CSS custom properties be animated or transitioned?

By default, no. CSS custom properties are treated as strings, so the browser cannot interpolate between values. However, using the @property rule (part of CSS Houdini), you can register a property with a specific syntax like <length>, <color>, or <number>. Once registered, the browser knows the type and can smoothly animate or transition between values. This works in Chrome, Edge, Safari 15.4+, and Firefox 128+.

Do CSS variables affect performance?

The performance impact of CSS variables is negligible for typical use cases. The browser resolves variable references during style computation, which adds a tiny overhead compared to hard-coded values. However, deeply nested variable references (var(--a) referencing var(--b) referencing var(--c)...) can increase computation time. In practice, even complex design systems with hundreds of variables perform well. The runtime flexibility they provide far outweighs the minimal performance cost.

Can I use CSS variables inside media queries or selectors?

You can change the value of a CSS variable inside a media query (e.g., @media (max-width: 768px) { :root { --font-size: 14px; } }), and it will automatically update everywhere that variable is used. However, you cannot use var() in the media query condition itself (e.g., @media (max-width: var(--breakpoint)) does NOT work). You also cannot use var() in selector strings. Variables work only in property values.

How do CSS variables work with the Shadow DOM in Web Components?

CSS custom properties are one of the few styling mechanisms that penetrate the Shadow DOM boundary. While regular CSS selectors cannot reach inside a shadow root, inherited custom properties flow through naturally. This makes CSS variables the recommended API for theming Web Components: the host page sets variables (--button-bg, --button-color), and the component's internal styles use var(--button-bg, defaultValue) to read them. This provides a clean, explicit styling API without breaking encapsulation.

CSS custom properties have transformed how we approach styling on the web. They bring variables, theming, and dynamic styling capabilities natively to the browser. Combined with @property for type-safe animations and JavaScript interoperability, they are an essential tool for every modern web developer.

Build your color palette with our Color Palette Generator →

Color Converter | Color Palette Generator | Tailwind Colors

𝕏 Twitterin LinkedIn
¿Fue útil?

Mantente actualizado

Recibe consejos de desarrollo y nuevas herramientas.

Sin spam. Cancela cuando quieras.

Prueba estas herramientas relacionadas

🎨Color Converter🎨Color Palette GeneratorTWTailwind CSS Color Picker{ }CSS Minifier / Beautifier

Artículos relacionados

Guía de degradados CSS: De lo básico a técnicas avanzadas

Domina los degradados CSS: lineales, radiales, cónicos, repetitivos con ejemplos prácticos, efectos de texto y rendimiento.

CSS Media Queries y breakpoints en 2025

Patrones modernos de media queries CSS y breakpoints para 2025. Container queries, consultas de preferencia y diseño responsive.