CSS Variable Generator: From Hardcoded Values to Design Systems#

Last year I inherited a legacy project. Opened the CSS file and found #3b82f6, #8b5cf6 scattered everywhere. Changing the primary color meant global search-and-replace, praying nothing was missed. After refactoring with CSS variables, changing colors finally became stress-free.

The Nature of CSS Variables#

CSS Custom Properties (commonly called CSS variables) have simple syntax:

:root {
  --color-primary: #3b82f6;
}

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

Unlike Sass/Less variables, CSS variables are runtime. This means:

// JavaScript can modify them dynamically
document.documentElement.style.setProperty('--color-primary', '#ef4444')

// Takes effect immediately - all usages update

This makes dark mode and theme switching remarkably simple.

Scope and Inheritance#

CSS variables follow CSS inheritance rules, definable at any level:

:root {
  --spacing: 1rem;        /* Global */
}

.card {
  --spacing: 0.5rem;      /* Override within card */
  padding: var(--spacing);
}

.card .item {
  margin: var(--spacing); /* Inherits card's 0.5rem */
}

This enables “local themes”:

:root {
  --text-color: #1f2937;
}

.dark-theme {
  --text-color: #f9fafb;
}

/* Force dark mode in a specific area */
.sidebar {
  --text-color: #f9fafb;
}

Naming Convention Pitfalls#

Early on, I named variables carelessly - --blue, --mainColor. Maintenance became painful. When --blue changed to red, the name contradicted the value.

Current conventions I follow:

:root {
  /* Semantic naming: purpose, not appearance */
  --color-primary: #3b82f6;
  --color-danger: #ef4444;
  --color-text: #1f2937;
  --color-text-muted: #6b7280;

  /* Layered naming: module-property-variant */
  --button-bg: var(--color-primary);
  --button-bg-hover: #2563eb;
  --button-radius: 0.5rem;

  /* Spacing system: multiplicative relationships */
  --spacing-xs: 0.25rem;
  --spacing-sm: 0.5rem;
  --spacing-md: 1rem;
  --spacing-lg: 2rem;
}

Names serve as documentation. Seeing --color-danger immediately conveys meaning without checking the value.

Default Values and Fallbacks#

var() supports default values:

.button {
  /* If --btn-bg is undefined, use #3b82f6 */
  background: var(--btn-bg, #3b82f6);
}

More practical for progressive enhancement:

.card {
  /* Older browsers: fallback */
  background: white;
  /* Modern browsers: variable */
  background: var(--card-bg, white);
}

Chained fallbacks are also possible:

.element {
  color: var(--theme-color, var(--brand-color, #333));
}

Dark Mode in Practice#

CSS variables make dark mode elegant:

:root {
  --bg: #ffffff;
  --text: #1f2937;
  --border: #e5e7eb;
}

@media (prefers-color-scheme: dark) {
  :root {
    --bg: #1f2937;
    --text: #f9fafb;
    --border: #374151;
  }
}

/* Or JS-controlled */
[data-theme="dark"] {
  --bg: #1f2937;
  --text: #f9fafb;
}

Component code remains unchanged:

.card {
  background: var(--bg);
  color: var(--text);
  border: 1px solid var(--border);
}

Performance Considerations#

CSS variables have overhead, but it’s usually negligible. Key considerations:

1. Avoid excessive use on critical paths#

/* Not recommended: variables for every property */
.element {
  margin: var(--m);
  padding: var(--p);
  border: var(--b);
  background: var(--bg);
  color: var(--c);
}

/* Recommended: only where dynamic values are needed */
.element {
  margin: 1rem;
  padding: 0.5rem;
  border: 1px solid var(--border-color);
  background: var(--bg-color);
}

2. Compute at definition time#

/* Not recommended: calculation on every use */
:root {
  --base: 1rem;
}
.element {
  padding: calc(var(--base) * 2);
}

/* Recommended: pre-computed */
:root {
  --base: 1rem;
  --double: 2rem;
}
.element {
  padding: var(--double);
}

Implementation Approach#

Based on these practices, I built a CSS Variable Generator:

Core functionality:

interface CssVariable {
  name: string    // e.g., --color-primary
  value: string   // e.g., #3b82f6
  category: string // e.g., colors, spacing
}

// Generate grouped output
function generateCss(variables: CssVariable[], selector: string): string {
  const grouped: Record<string, CssVariable[]> = {}
  
  variables.forEach(v => {
    if (!grouped[v.category]) grouped[v.category] = []
    grouped[v.category].push(v)
  })
  
  let css = `${selector} {\n`
  Object.entries(grouped).forEach(([cat, vars]) => {
    css += `\n  /* ${cat} */\n`
    vars.forEach(v => {
      css += `  ${v.name}: ${v.value};\n`
    })
  })
  css += '}\n'
  
  return css
}

Key features:

  1. Color detection: Recognizes #hex, rgb(), hsl() formats, shows color preview
  2. Category management: Groups output by colors, spacing, typography, etc.
  3. Custom selectors: Supports :root, .dark, [data-theme="dark"], etc.

Migrating from Preprocessors#

When migrating from Sass/Less, a hybrid approach works well:

// Keep Sass variables for calculations
$base-spacing: 8px;

// Output CSS variables for runtime
:root {
  --spacing: #{$base-spacing};
  --spacing-lg: #{$base-spacing * 2};
}

This preserves Sass’s calculation power while gaining CSS variables’ dynamic capabilities.


CSS variables aren’t a silver bullet, but for design systems, theme switching, and component libraries, they offer more flexibility than hardcoded values or preprocessor variables. The key is establishing naming conventions - don’t let variables become another maintenance burden.

Related tools: CSS Gradient Generator | Color Contrast Checker