Skillversitas

CSS Variables (Custom Properties)

CSS variables, also known as custom properties, let you define values that can be reused throughout a stylesheet. They start with -- and are accessed using the var() function. CSS variables are the modern way to manage values such as colors, spacing, font sizes, and breakpoints.

The advantages of CSS variables over preprocessor (Sass/Less) variables:

  • Work in the browser without compilation
  • Can be changed dynamically with JavaScript
  • Have scope (global and local)
  • Support fallback values
  • Can be inherited by child elements

Key characteristics:

  • Declared using --variable-name: value;
  • Accessed using var(--variable-name)
  • Declared globally in :root, locally in any selector
  • Can be changed via JavaScript with element.style.setProperty()

Code Example

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>CSS Variables</title>
  <style>
    /* ===== Global Variables ===== */
    :root {
      --color-primary: #667eea;
      --color-secondary: #764ba2;
      --color-accent: #f5576c;
      --color-success: #43e97b;
      --color-bg: #f0f2f5;
      --color-text: #1a1a2e;
      --color-text-light: #666;
      --color-white: #ffffff;

      --space-xs: 4px;
      --space-sm: 8px;
      --space-md: 16px;
      --space-lg: 24px;
      --space-xl: 32px;
      --space-2xl: 48px;

      --radius-sm: 4px;
      --radius-md: 8px;
      --radius-lg: 12px;
      --radius-xl: 16px;
      --radius-full: 50%;

      --font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
      --font-size-sm: 0.85rem;
      --font-size-base: 1rem;
      --font-size-lg: 1.2rem;
      --font-size-xl: 1.8rem;
      --font-size-2xl: 2.5rem;

      --shadow-sm: 0 2px 8px rgba(0,0,0,0.06);
      --shadow-md: 0 4px 15px rgba(0,0,0,0.1);
      --shadow-lg: 0 8px 30px rgba(0,0,0,0.15);

      --transition: 0.3s ease;
    }

    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }

    body {
      font-family: var(--font-family);
      background: var(--color-bg);
      color: var(--color-text);
      line-height: 1.6;
      transition: background var(--transition), color var(--transition);
    }

    /* ===== Theme Toggle (Dark Mode) ===== */
    body.dark-mode {
      --color-bg: #0f0f1a;
      --color-text: #e0e0e0;
      --color-text-light: #aaa;
      --color-white: #1a1a2e;
      --shadow-sm: 0 2px 8px rgba(0,0,0,0.3);
      --shadow-md: 0 4px 15px rgba(0,0,0,0.4);
    }

    .container {
      max-width: 900px;
      margin: 0 auto;
      padding: 40px 20px;
    }

    /* ===== Header with Theme Toggle ===== */
    .header {
      display: flex;
      justify-content: space-between;
      align-items: center;
      margin-bottom: 40px;
    }

    .header h1 {
      font-size: var(--font-size-2xl);
      color: var(--color-primary);
    }

    .theme-toggle {
      padding: var(--space-sm) var(--space-md);
      background: var(--color-primary);
      color: var(--color-white);
      border: none;
      border-radius: var(--radius-md);
      cursor: pointer;
      font-size: var(--font-size-sm);
      font-weight: 600;
      transition: background var(--transition), transform var(--transition);
    }

    .theme-toggle:hover {
      background: var(--color-secondary);
      transform: translateY(-2px);
    }

    /* ===== Section: Color Palette ===== */
    .section {
      margin-bottom: var(--space-2xl);
    }

    .section h2 {
      font-size: var(--font-size-xl);
      margin-bottom: var(--space-md);
      color: var(--color-text);
    }

    .color-palette {
      display: grid;
      grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
      gap: var(--space-md);
    }

    .color-swatch {
      height: 100px;
      border-radius: var(--radius-lg);
      display: flex;
      align-items: flex-end;
      padding: var(--space-sm);
      color: white;
      font-size: var(--font-size-sm);
      font-weight: 600;
    }

    .swatch-primary { background: var(--color-primary); }
    .swatch-secondary { background: var(--color-secondary); }
    .swatch-accent { background: var(--color-accent); }
    .swatch-success { background: var(--color-success); color: #1a1a2e; }

    /* ===== Section: Spacing System ===== */
    .spacing-demo {
      background: var(--color-white);
      border-radius: var(--radius-lg);
      padding: var(--space-lg);
      box-shadow: var(--shadow-sm);
    }

    .spacing-item {
      display: flex;
      align-items: center;
      margin-bottom: var(--space-md);
    }

    .spacing-item:last-child {
      margin-bottom: 0;
    }

    .spacing-label {
      width: 100px;
      font-size: var(--font-size-sm);
      font-family: monospace;
      color: var(--color-text-light);
    }

    .spacing-bar {
      height: 24px;
      background: linear-gradient(135deg, var(--color-primary), var(--color-secondary));
      border-radius: var(--radius-sm);
      transition: width var(--transition);
    }

    /* ===== Section: Component Library ===== */
    .component-grid {
      display: grid;
      grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
      gap: var(--space-lg);
    }

    .component-card {
      background: var(--color-white);
      border-radius: var(--radius-lg);
      padding: var(--space-lg);
      box-shadow: var(--shadow-sm);
      transition: transform var(--transition), box-shadow var(--transition);
    }

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

    .component-card h3 {
      color: var(--color-text);
      margin-bottom: var(--space-sm);
    }

    .component-card p {
      color: var(--color-text-light);
      font-size: var(--font-size-sm);
      margin-bottom: var(--space-md);
    }

    /* Buttons using variables */
    .btn {
      padding: var(--space-sm) var(--space-lg);
      border: none;
      border-radius: var(--radius-md);
      font-size: var(--font-size-sm);
      font-weight: 600;
      cursor: pointer;
      transition: all var(--transition);
    }

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

    .btn-primary:hover {
      background: var(--color-secondary);
      transform: translateY(-1px);
    }

    .btn-accent {
      background: var(--color-accent);
      color: var(--color-white);
    }

    .btn-success {
      background: var(--color-success);
      color: var(--color-text);
    }

    /* Input using variables */
    .form-input {
      width: 100%;
      padding: var(--space-sm) var(--space-md);
      border: 2px solid #ddd;
      border-radius: var(--radius-md);
      font-size: var(--font-size-base);
      font-family: var(--font-family);
      transition: border-color var(--transition), box-shadow var(--transition);
      background: transparent;
      color: var(--color-text);
    }

    .form-input:focus {
      outline: none;
      border-color: var(--color-primary);
      box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.2);
    }

    /* Badge using variables */
    .badge {
      display: inline-block;
      padding: var(--space-xs) var(--space-sm);
      border-radius: var(--radius-sm);
      font-size: 0.75rem;
      font-weight: 600;
    }

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

    .badge-accent {
      background: var(--color-accent);
      color: var(--color-white);
    }

    /* ===== Section: Scoped Variables ===== */
    .scope-demo {
      display: grid;
      grid-template-columns: 1fr 1fr;
      gap: var(--space-lg);
    }

    .scope-box {
      padding: var(--space-lg);
      border-radius: var(--radius-lg);
    }

    .scope-box-a {
      --local-color: #667eea;
      --local-size: 1.5rem;
      background: rgba(102, 126, 234, 0.1);
      border: 2px solid var(--local-color);
    }

    .scope-box-b {
      --local-color: #f5576c;
      --local-size: 1rem;
      background: rgba(245, 87, 108, 0.1);
      border: 2px solid var(--local-color);
    }

    .scope-box h3 {
      color: var(--local-color);
      font-size: var(--local-size);
      margin-bottom: var(--space-sm);
    }

    .scope-box p {
      color: var(--color-text-light);
      font-size: var(--font-size-sm);
    }

    /* ===== Section: Dynamic with JS ===== */
    .dynamic-section {
      background: var(--color-white);
      border-radius: var(--radius-lg);
      padding: var(--space-xl);
      box-shadow: var(--shadow-sm);
    }

    .slider-group {
      margin-bottom: var(--space-lg);
    }

    .slider-group label {
      display: block;
      margin-bottom: var(--space-sm);
      font-weight: 600;
      font-size: var(--font-size-sm);
      color: var(--color-text);
    }

    .slider-group input[type="range"] {
      width: 100%;
      accent-color: var(--color-primary);
    }

    .preview-box {
      width: 100%;
      height: 150px;
      background: var(--color-primary);
      border-radius: var(--radius-lg);
      display: flex;
      align-items: center;
      justify-content: center;
      color: white;
      font-weight: bold;
      font-size: var(--font-size-lg);
      transition: all 0.1s ease;
    }
  </style>
</head>
<body>

  <div class="container">

    <div class="header">
      <h1>CSS Variables</h1>
      <button class="theme-toggle" onclick="document.body.classList.toggle('dark-mode')">
        🌓 Toggle Dark Mode
      </button>
    </div>

    <div class="section">
      <h2>Color Palette</h2>
      <div class="color-palette">
        <div class="color-swatch swatch-primary">Primary</div>
        <div class="color-swatch swatch-secondary">Secondary</div>
        <div class="color-swatch swatch-accent">Accent</div>
        <div class="color-swatch swatch-success">Success</div>
      </div>
    </div>

    <div class="section">
      <h2>Spacing System</h2>
      <div class="spacing-demo">
        <div class="spacing-item">
          <span class="spacing-label">--space-xs</span>
          <div class="spacing-bar" style="width: 4px;"></div>
        </div>
        <div class="spacing-item">
          <span class="spacing-label">--space-sm</span>
          <div class="spacing-bar" style="width: 8px;"></div>
        </div>
        <div class="spacing-item">
          <span class="spacing-label">--space-md</span>
          <div class="spacing-bar" style="width: 16px;"></div>
        </div>
        <div class="spacing-item">
          <span class="spacing-label">--space-lg</span>
          <div class="spacing-bar" style="width: 24px;"></div>
        </div>
        <div class="spacing-item">
          <span class="spacing-label">--space-xl</span>
          <div class="spacing-bar" style="width: 32px;"></div>
        </div>
        <div class="spacing-item">
          <span class="spacing-label">--space-2xl</span>
          <div class="spacing-bar" style="width: 48px;"></div>
        </div>
      </div>
    </div>

    <div class="section">
      <h2>Component Library</h2>
      <div class="component-grid">
        <div class="component-card">
          <h3>Buttons</h3>
          <p>Buttons with various color variants using CSS variables.</p>
          <button class="btn btn-primary">Primary</button>
          <button class="btn btn-accent" style="margin-left:8px;">Accent</button>
          <button class="btn btn-success" style="margin-left:8px;">Success</button>
        </div>
        <div class="component-card">
          <h3>Form Input</h3>
          <p>Input fields with focus states using variables.</p>
          <input type="text" class="form-input" placeholder="Ketik sesuatu..." style="margin-bottom:10px;">
          <div>
            <span class="badge badge-primary" >New</span>
            <span class="badge badge-accent" style="margin-left:5px;">Populer</span>
          </div>
        </div>
        <div class="component-card">
          <h3>Card</h3>
          <p>Cards using shadows and border-radius from CSS variables. Hover for the effect.</p>
        </div>
      </div>
    </div>

    <div class="section">
      <h2>Scoped Variables</h2>
      <div class="scope-demo">
        <div class="scope-box scope-box-a">
          <h3>Box A (Biru)</h3>
          <p>The --local-color and --local-size variables are defined at this level. Different from Box B.</p>
        </div>
        <div class="scope-box scope-box-b">
          <h3>Box B (Merah)</h3>
          <p>The same variable with a different name. The local scope overrides the parent scope.</p>
        </div>
      </div>
    </div>

    <div class="section">
      <h2>Dynamic Variables with JavaScript</h2>
      <div class="dynamic-section">
        <div class="slider-group">
          <label>Border Radius: <span id="radiusVal">12</span>px</label>
          <input type="range" min="0" max="50" value="12" oninput="updateVar('--dynamic-radius', this.value + 'px'); document.getElementById('radiusVal').textContent = this.value">
        </div>
        <div class="slider-group">
          <label>Scale: <span id="scaleVal">1</span></label>
          <input type="range" min="50" max="150" value="100" oninput="updateVar('--dynamic-scale', this.value/100); document.getElementById('scaleVal').textContent = (this.value/100).toFixed(1)">
        </div>
        <div class="preview-box" id="previewBox">Preview</div>
      </div>
    </div>

  </div>

  <script>
    function updateVar(name, value) {
      document.documentElement.style.setProperty(name, value);
    }

    document.documentElement.style.setProperty('--dynamic-radius', '12px');
    document.documentElement.style.setProperty('--dynamic-scale', '1');
  </script>

  <style>
    #previewBox {
      border-radius: var(--dynamic-radius, 12px);
      transform: scale(var(--dynamic-scale, 1));
    }
  </style>

</body>
</html>

Code Explanation

Declaring CSS Variables

CSS variables are declared with the -- prefix and accessed with var():

:root {
  --color-primary: #667eea;
  --spacing: 16px;
}

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

Global vs Local Scope

Variables declared in :root are global — they can be accessed anywhere. Variables in a specific selector are local and only apply to that element and its children.

/* Global - accessible anywhere */
:root {
  --font-size: 16px;
}

/* Local - only applies to .card and its children */
.card {
  --card-padding: 20px;
  padding: var(--card-padding);
}

A local variable overrides the global variable with the same name:

:root { --color: blue; }
.card { --color: red; } /* .card uses red, everything else blue */

Fallback Values

var() supports fallback values — a second value as a backup if the variable is not defined:

.element {
  /* fallback: #333 if --color-text does not exist */
  color: var(--color-text, #333);

  /* multiple fallback */
  font-size: var(--font-size, var(--fallback-size, 16px));
}

Dynamic Variables via JavaScript

CSS variables can be changed dynamically using JavaScript:

// Change the value
document.documentElement.style.setProperty('--color-primary', '#f5576c');

// Change on a specific element
document.querySelector('.card').style.setProperty('--card-bg', '#fff');

// Read the value
const value = getComputedStyle(document.documentElement)
  .getPropertyValue('--color-primary');

Spacing System

Define consistent spacing as variables:

:root {
  --space-1: 4px;
  --space-2: 8px;
  --space-3: 12px;
  --space-4: 16px;
  --space-6: 24px;
  --space-8: 32px;
  --space-12: 48px;
}

.card { padding: var(--space-6); }
.button { padding: var(--space-3) var(--space-6); }

Theme Switching

CSS variables make dark mode implementation easy — just change the values in the .dark-mode class:

:root {
  --bg: white;
  --text: #333;
}

.dark-mode {
  --bg: #1a1a2e;
  --text: #e0e0e0;
}

body { background: var(--bg); color: var(--text); }
document.body.classList.toggle('dark-mode');

Everyday Use Cases

1. Design Token System

CSS variables work as design tokens — a single source of truth for all design values:

:root {
  /* Colors */
  --primary-50: #eef2ff;
  --primary-500: #667eea;
  --primary-900: #3730a3;

  /* Typography */
  --text-xs: 0.75rem;
  --text-sm: 0.875rem;
  --text-base: 1rem;
  --text-lg: 1.125rem;

  /* Shadows */
  --shadow-sm: 0 1px 2px rgba(0,0,0,0.05);
  --shadow-md: 0 4px 6px rgba(0,0,0,0.1);
}

2. Consistent Spacing Scale

Creating uniform spacing across an entire page:

:root {
  --space-unit: 4px;
}

.element-1 { margin: calc(var(--space-unit) * 1); }  /* 4px */
.element-2 { margin: calc(var(--space-unit) * 2); }  /* 8px */
.element-3 { margin: calc(var(--space-unit) * 4); }  /* 16px */

3. Dark Mode without Duplication

One set of variables changes the entire theme — no need to duplicate CSS:

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

.card {
  background: var(--bg-card);
  box-shadow: var(--shadow-card);
}

/* Everything changes automatically when dark mode is active */
body.dark {
  --bg-primary: #0f0f1a;
  --bg-card: #1a1a2e;
  --text-primary: #e0e0e0;
}

4. Reusable Components

Each component pulls its values from variables — changing one variable changes every instance:

:root {
  --btn-primary-bg: #667eea;
  --btn-radius: 8px;
}

.btn-primary { background: var(--btn-primary-bg); border-radius: var(--btn-radius); }

/* Change once, all buttons change */
:root { --btn-primary-bg: #f5576c; }

5. Responsive Spacing via JavaScript

Spacing that changes based on the viewport, using JavaScript to set a CSS variable:

function updateSpacing() {
  const width = window.innerWidth;
  const spacing = width < 480 ? 12 : width < 768 ? 16 : 24;
  document.documentElement.style.setProperty('--page-spacing', spacing + 'px');
}

window.addEventListener('resize', updateSpacing);
updateSpacing();

Conclusion

CSS variables (--name: value, accessed with var(--name)) are the modern way to manage reusable design values. Declare them globally in :root or locally in any selector. They support fallback values (var(--name, fallback)) and can be changed dynamically via JavaScript (element.style.setProperty()). Most useful for: theme switching (dark mode), spacing systems, color palettes, design tokens, and reusable components. CSS variables eliminate the need to hardcode the same value in many places — change one variable and every component is affected.