Web Development

CSS Variables: Membangun Design System

Pelajari CSS Custom Properties (Variables) secara mendalam — membangun design system yang scalable, theming dark/light mode, design tokens, komponen reusable, dan teknik manajemen style modern untuk proyek profesional

1. Pengenalan CSS Variables

CSS Custom Properties (juga dikenal sebagai CSS Variables) adalah fitur CSS yang memungkinkan kamu mendefinisikan nilai yang bisa digunakan kembali dan dimodifikasi secara dinamis. Berbeda dengan variabel di preprocessor (Sass, Less), CSS Variables hidup di browser dan bisa diubah secara real-time.

CSS Variables vs Preprocessor Variables

  • Aspek CSS Variables Sass/Less Variables
    RuntimeBerjalan di browser (live)Dikompilasi ke CSS (statis)
    Dynamic✅ Bisa diubah via JS/CSS❌ Hanya saat compile
    Cascade✅ Mengikuti CSS cascade❌ Flat, tidak ada cascade
    Inheritance✅ Diwarisi ke child elements❌ Tidak ada inheritance
    Theming✅ Sangat mudah⚠️ Perlu rebuild
    Fallbackvar(--x, fallback)⚠️ Perlu mixin
    PerformanceNative browserPerlu compilation step
    Diagram: CSS Variables Flow
      ┌───────────────────────────────────────────────────┐
      │              CSS VARIABLES PIPELINE                │
      │                                                    │
      │  ┌─────────────┐     ┌──────────────┐             │
      │  │  :root       │     │   Component  │             │
      │  │  --color:    │────→│   .card {    │             │
      │  │    #3b82f6   │     │    color:    │             │
      │  └─────────────┘     │    var(--color)│             │
      │         │             └──────────────┘             │
      │         │                                           │
      │         ↓                                           │
      │  ┌─────────────┐     ┌──────────────┐             │
      │  │ JavaScript   │     │   Browser    │             │
      │  │ el.style.    │────→│   Resolves   │             │
      │  │ setProperty  │     │   Values     │             │
      │  │('--color',   │     │   Live!      │             │
      │  │ '#10b981')   │     └──────────────┘             │
      │  └─────────────┘                                   │
      └───────────────────────────────────────────────────┘
    

    2. Dasar Custom Properties

    Deklarasi dan Penggunaan

    /* Deklarasi variabel di :root (global) */
    :root {
      --primary-color: #3b82f6;
      --secondary-color: #8b5cf6;
      --success-color: #10b981;
      --danger-color: #ef4444;
      --text-color: #1f2937;
      --bg-color: #ffffff;
      --font-family: 'Inter', -apple-system, sans-serif;
      --font-size-base: 16px;
      --spacing-unit: 8px;
      --border-radius: 8px;
      --shadow-sm: 0 1px 2px rgba(0,0,0,0.05);
      --shadow-md: 0 4px 6px rgba(0,0,0,0.1);
      --shadow-lg: 0 10px 25px rgba(0,0,0,0.15);
      --transition-fast: 0.15s ease;
      --transition-normal: 0.3s ease;
      --max-width: 1200px;
    }
    
    /* Menggunakan variabel */
    body {
      font-family: var(--font-family);
      font-size: var(--font-size-base);
      color: var(--text-color);
      background-color: var(--bg-color);
      line-height: 1.6;
    }
    
    .btn-primary {
      background-color: var(--primary-color);
      color: white;
      padding: calc(var(--spacing-unit) * 1.5) calc(var(--spacing-unit) * 3);
      border: none;
      border-radius: var(--border-radius);
      transition: all var(--transition-normal);
      box-shadow: var(--shadow-sm);
    }
    
    .btn-primary:hover {
      box-shadow: var(--shadow-md);
    }
    
    /* Fallback value jika variabel tidak didefinisikan */
    .element {
      color: var(--undefined-color, #333);
      /* Jika --undefined-color tidak ada, gunakan #333 */
      font-size: var(--font-size-base, 16px);
    }
    
    /* Multiple fallbacks */
    .multi-fallback {
      color: var(--custom-color, var(--primary-color, blue));
      /* 1. Coba --custom-color
         2. Jika tidak ada, coba --primary-color
         3. Jika tidak ada, gunakan blue */
    }

    Property yang Bisa Di-Variabel-kan

    /* ✅ Hampir semua property bisa menggunakan variabel */
    :root {
      --radius-sm: 4px;
      --radius-md: 8px;
      --radius-lg: 16px;
      --radius-full: 9999px;
    
      --space-1: 4px;
      --space-2: 8px;
      --space-3: 12px;
      --space-4: 16px;
      --space-6: 24px;
      --space-8: 32px;
      --space-12: 48px;
      --space-16: 64px;
    
      --text-xs: 0.75rem;
      --text-sm: 0.875rem;
      --text-base: 1rem;
      --text-lg: 1.125rem;
      --text-xl: 1.25rem;
      --text-2xl: 1.5rem;
      --text-3xl: 1.875rem;
      --text-4xl: 2.25rem;
    }
    
    /* Grid template dengan variabel */
    .grid {
      display: grid;
      grid-template-columns: repeat(var(--grid-cols, 3), 1fr);
      gap: var(--space-4);
    }
    
    /* Gradient dengan variabel */
    .gradient-element {
      background: linear-gradient(
        var(--gradient-angle, 135deg),
        var(--primary-color),
        var(--secondary-color)
      );
    }
    
    /* Media query — ⚠️ Tidak bisa langsung di @media */
    /* ❌ */
    /* @media (min-width: var(--breakpoint-md)) {} */
    
    /* ✅ Tapi bisa di container queries */
    .container {
      container-type: inline-size;
    }
    @container (min-width: 768px) {
      .card { grid-template-columns: 1fr 1fr; }
    }
    
    /* ✅ Variabel bisa di media query body */
    @media (min-width: 768px) {
      :root {
        --font-size-base: 18px;
        --grid-cols: 4;
        --spacing-unit: 12px;
      }
    }

    Variabel untuk Komputasi

    /* Variabel dalam calc() */
    .layout {
      --sidebar-width: 280px;
      --gap: 24px;
    
      display: grid;
      grid-template-columns: var(--sidebar-width) calc(100% - var(--sidebar-width) - var(--gap));
      gap: var(--gap);
    }
    
    /* Responsive sidebar */
    @media (max-width: 768px) {
      .layout {
        --sidebar-width: 100%;
      }
    }
    
    /* Variabel dalam color functions */
    :root {
      --hue: 220;
      --saturation: 90%;
      --lightness: 55%;
    }
    
    .primary {
      /* Membuat warna dari HSL */
      color: hsl(var(--hue), var(--saturation), var(--lightness));
    }
    
    .primary-light {
      /* Variasi lebih terang */
      color: hsl(var(--hue), var(--saturation), calc(var(--lightness) + 20%));
    }
    
    .primary-dark {
      /* Variasi lebih gelap */
      color: hsl(var(--hue), var(--saturation), calc(var(--lightness) - 15%));
    }
    
    /* Clamp dengan variabel */
    .responsive-text {
      font-size: clamp(
        var(--text-min, 14px),
        var(--text-fluid, 3vw),
        var(--text-max, 24px)
      );
    }

    3. Scoping & Inheritance

    CSS Variables mengikuti aturan CSS cascade dan inheritance. Kamu bisa mendefinisikan variabel di berbagai level dan menimpa (override) sesuai kebutuhan.

    Global vs Local Scope

    /* Global scope (seluruh halaman) */
    :root {
      --primary: #3b82f6;
      --bg: #ffffff;
      --text: #1f2937;
    }
    
    /* Section scope */
    .hero {
      --bg: #1e3a5f;
      --text: #ffffff;
      /* Variabel ini hanya berlaku di .hero dan children-nya */
    }
    
    /* Component scope */
    .card {
      --card-padding: 24px;
      --card-bg: var(--bg);  /* Menggunakan global */
      padding: var(--card-padding);
      background: var(--card-bg);
      color: var(--text);
    }
    
    /* Modifier scope */
    .card--featured {
      --card-padding: 32px;
      --card-bg: linear-gradient(135deg, #667eea, #764ba2);
    }
    
    /* Deep override */
    .sidebar {
      --text: #9ca3af;
      color: var(--text);
    }
    
    .sidebar .active-link {
      --text: var(--primary);
      color: var(--text);
    }
    
    <!-- HTML: Variabel mengikuti scope terdekat -->
    <div class="hero">
      <p>Teks putih (dari .hero scope)</p>
      <div class="card">
        <p>Teks putih (diwarisi dari .hero)</p>
      </div>
    </div>
    <div class="card">
      <p>Teks gelap (dari :root scope)</p>
    </div>

    Cascade Specificity dengan Variabel

    /* Variabel mengikuti specificity */
    :root {
      --color: blue;
    }
    
    .section {
      --color: green;
    }
    
    .highlight {
      --color: red;
    }
    
    /* Jika elemen memiliki class .section DAN .highlight,
       specificity yang lebih tinggi menang */
    .text {
      color: var(--color);
    }
    
    /* .section .highlight .text → --color: red (lebih spesifik) */
    
    /* Ini sangat powerful untuk component theming */
    [data-theme="dark"] {
      --bg-primary: #0f172a;
      --text-primary: #e2e8f0;
    }
    
    [data-theme="light"] {
      --bg-primary: #ffffff;
      --text-primary: #1e293b;
    }
    
    /* Komponen tidak perlu tahu tema mana yang aktif */
    .card {
      background: var(--bg-primary);
      color: var(--text-primary);
    }

    4. Dynamic Variables dengan JavaScript

    Salah satu kekuatan terbesar CSS Variables adalah bisa dimanipulasi melalui JavaScript secara real-time. Ini memungkinkan interaksi dinamis tanpa perlu mengubah class atau inline styles.

    Membaca dan Menulis CSS Variables

    // Membaca variabel dari :root
    const root = document.documentElement;
    const primaryColor = getComputedStyle(root)
      .getPropertyValue('--primary-color').trim();
    console.log(primaryColor); // '#3b82f6'
    
    // Menulis variabel ke :root
    root.style.setProperty('--primary-color', '#10b981');
    
    // Menghapus variabel
    root.style.removeProperty('--primary-color');
    
    // Menulis ke elemen spesifik
    const card = document.querySelector('.card');
    card.style.setProperty('--card-padding', '32px');
    
    // Menggunakan CSS Variables untuk animasi dari JS
    const progress = document.querySelector('.progress-bar');
    let value = 0;
    
    function updateProgress() {
      value += 1;
      progress.style.setProperty('--progress', `${value}%`);
      if (value < 100) requestAnimationFrame(updateProgress);
    }
    
    // Baca dari elemen tertentu
    const el = document.querySelector('.custom-element');
    const styles = getComputedStyle(el);
    const customVal = styles.getPropertyValue('--my-var').trim();

    Interaktif Theme Switcher

    // Theme switcher dengan CSS Variables
    class ThemeSwitcher {
      constructor() {
        this.themes = {
          light: {
            '--bg-primary': '#ffffff',
            '--bg-secondary': '#f8fafc',
            '--text-primary': '#1e293b',
            '--text-secondary': '#64748b',
            '--border-color': '#e2e8f0',
            '--accent': '#3b82f6'
          },
          dark: {
            '--bg-primary': '#0f172a',
            '--bg-secondary': '#1e293b',
            '--text-primary': '#e2e8f0',
            '--text-secondary': '#94a3b8',
            '--border-color': '#334155',
            '--accent': '#60a5fa'
          },
          sepia: {
            '--bg-primary': '#f5f0e8',
            '--bg-secondary': '#ebe4d4',
            '--text-primary': '#433422',
            '--text-secondary': '#7a6a52',
            '--border-color': '#d4c5a9',
            '--accent': '#b8860b'
          }
        };
      }
    
      apply(themeName) {
        const theme = this.themes[themeName];
        if (!theme) return;
    
        Object.entries(theme).forEach(([prop, value]) => {
          document.documentElement.style.setProperty(prop, value);
        });
    
        // Juga set data attribute untuk conditional CSS
        document.documentElement.setAttribute('data-theme', themeName);
    
        // Simpan preferensi
        localStorage.setItem('theme', themeName);
      }
    
      init() {
        const saved = localStorage.getItem('theme');
        const prefersDark = matchMedia('(prefers-color-scheme: dark)').matches;
        this.apply(saved || (prefersDark ? 'dark' : 'light'));
      }
    }
    
    // Penggunaan
    const switcher = new ThemeSwitcher();
    switcher.init();
    
    document.getElementById('themeToggle').addEventListener('click', () => {
      const current = document.documentElement.getAttribute('data-theme');
      switcher.apply(current === 'dark' ? 'light' : 'dark');
    });

    Real-time Color Picker

    // Real-time color customization
    document.getElementById('colorPicker').addEventListener('input', (e) => {
      const color = e.target.value;
    
      // Generate color shades
      document.documentElement.style.setProperty('--primary', color);
    
      // Generate lighter shade (gunakan HSL)
      const hsl = hexToHSL(color);
      document.documentElement.style.setProperty(
        '--primary-light',
        `hsl(${hsl.h}, ${hsl.s}%, ${Math.min(hsl.l + 20, 95)}%)`
      );
      document.documentElement.style.setProperty(
        '--primary-dark',
        `hsl(${hsl.h}, ${hsl.s}%, ${Math.max(hsl.l - 15, 5)}%)`
      );
    });
    
    function hexToHSL(hex) {
      let r = parseInt(hex.slice(1,3), 16) / 255;
      let g = parseInt(hex.slice(3,5), 16) / 255;
      let b = parseInt(hex.slice(5,7), 16) / 255;
    
      const max = Math.max(r, g, b);
      const min = Math.min(r, g, b);
      let h, s, l = (max + min) / 2;
    
      if (max === min) {
        h = s = 0;
      } else {
        const d = max - min;
        s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
        switch (max) {
          case r: h = ((g - b) / d + (g < b ? 6 : 0)) / 6; break;
          case g: h = ((b - r) / d + 2) / 6; break;
          case b: h = ((r - g) / d + 4) / 6; break;
        }
      }
      return { h: Math.round(h*360), s: Math.round(s*100), l: Math.round(l*100) };
    }
    
    /* CSS yang responsif terhadap perubahan */
    .card {
      background: var(--primary-light);
      border: 2px solid var(--primary);
      color: var(--primary-dark);
      transition: all 0.3s ease;
    }
    .card:hover {
      background: var(--primary);
      color: white;
    }

    5. Design Tokens

    Design tokens adalah unit terkecil dari design system — nilai-nilai atomik yang menyimpan keputusan desain (warna, jarak, ukuran font, dll) dalam format yang konsisten dan reusable.

    Tiga Layer Design Tokens

    /* ============================================
       LAYER 1: PRIMITIVE TOKENS
       Nilai mentah yang tidak bermakna
       ============================================ */
    :root {
      /* Colors */
      --blue-50: #eff6ff;
      --blue-100: #dbeafe;
      --blue-200: #bfdbfe;
      --blue-300: #93c5fd;
      --blue-400: #60a5fa;
      --blue-500: #3b82f6;
      --blue-600: #2563eb;
      --blue-700: #1d4ed8;
      --blue-800: #1e40af;
      --blue-900: #1e3a8a;
    
      --gray-50: #f9fafb;
      --gray-100: #f3f4f6;
      --gray-200: #e5e7eb;
      --gray-300: #d1d5db;
      --gray-400: #9ca3af;
      --gray-500: #6b7280;
      --gray-600: #4b5563;
      --gray-700: #374151;
      --gray-800: #1f2937;
      --gray-900: #111827;
    
      /* Spacing scale (4px base) */
      --space-0: 0;
      --space-0-5: 2px;
      --space-1: 4px;
      --space-1-5: 6px;
      --space-2: 8px;
      --space-2-5: 10px;
      --space-3: 12px;
      --space-3-5: 14px;
      --space-4: 16px;
      --space-5: 20px;
      --space-6: 24px;
      --space-7: 28px;
      --space-8: 32px;
      --space-10: 40px;
      --space-12: 48px;
      --space-16: 64px;
      --space-20: 80px;
      --space-24: 96px;
    
      /* Font sizes */
      --text-2xs: 0.625rem;  /* 10px */
      --text-xs: 0.75rem;    /* 12px */
      --text-sm: 0.875rem;   /* 14px */
      --text-base: 1rem;     /* 16px */
      --text-lg: 1.125rem;   /* 18px */
      --text-xl: 1.25rem;    /* 20px */
      --text-2xl: 1.5rem;    /* 24px */
      --text-3xl: 1.875rem;  /* 30px */
      --text-4xl: 2.25rem;   /* 36px */
      --text-5xl: 3rem;      /* 48px */
    
      /* Font weights */
      --weight-normal: 400;
      --weight-medium: 500;
      --weight-semibold: 600;
      --weight-bold: 700;
    
      /* Border radius */
      --radius-none: 0;
      --radius-sm: 4px;
      --radius-md: 8px;
      --radius-lg: 12px;
      --radius-xl: 16px;
      --radius-2xl: 24px;
      --radius-full: 9999px;
    
      /* Shadows */
      --shadow-sm: 0 1px 2px 0 rgba(0,0,0,0.05);
      --shadow-md: 0 4px 6px -1px rgba(0,0,0,0.1);
      --shadow-lg: 0 10px 15px -3px rgba(0,0,0,0.1);
      --shadow-xl: 0 20px 25px -5px rgba(0,0,0,0.1);
    
      /* Z-index scale */
      --z-dropdown: 1000;
      --z-sticky: 1020;
      --z-fixed: 1030;
      --z-modal-backdrop: 1040;
      --z-modal: 1050;
      --z-popover: 1060;
      --z-tooltip: 1070;
      --z-toast: 1080;
    }
    
    /* ============================================
       LAYER 2: SEMANTIC TOKENS
       Nilai yang bermakna dan kontekstual
       ============================================ */
    :root {
      /* Semantic colors */
      --color-primary: var(--blue-500);
      --color-primary-hover: var(--blue-600);
      --color-primary-active: var(--blue-700);
      --color-primary-light: var(--blue-50);
      --color-primary-subtle: var(--blue-100);
    
      --color-text: var(--gray-900);
      --color-text-secondary: var(--gray-600);
      --color-text-muted: var(--gray-400);
      --color-text-inverse: var(--gray-50);
    
      --color-bg: var(--gray-50);
      --color-bg-elevated: white;
      --color-bg-sunken: var(--gray-100);
    
      --color-border: var(--gray-200);
      --color-border-strong: var(--gray-300);
    
      --color-success: #10b981;
      --color-warning: #f59e0b;
      --color-error: #ef4444;
      --color-info: #3b82f6;
    
      /* Semantic spacing */
      --spacing-page: var(--space-6);
      --spacing-section: var(--space-16);
      --spacing-component: var(--space-4);
      --spacing-element: var(--space-2);
    
      /* Semantic typography */
      --font-heading: 'Plus Jakarta Sans', sans-serif;
      --font-body: 'Inter', sans-serif;
      --font-mono: 'JetBrains Mono', monospace;
      --line-height-heading: 1.2;
      --line-height-body: 1.6;
    }
    
    /* ============================================
       LAYER 3: COMPONENT TOKENS
       Nilai spesifik untuk komponen
       ============================================ */
    :root {
      /* Button */
      --btn-height-sm: 32px;
      --btn-height-md: 40px;
      --btn-height-lg: 48px;
      --btn-padding-sm: var(--space-2) var(--space-3);
      --btn-padding-md: var(--space-2-5) var(--space-5);
      --btn-padding-lg: var(--space-3) var(--space-6);
      --btn-radius: var(--radius-md);
      --btn-font-weight: var(--weight-medium);
    
      /* Card */
      --card-padding: var(--space-6);
      --card-radius: var(--radius-lg);
      --card-shadow: var(--shadow-md);
      --card-bg: var(--color-bg-elevated);
      --card-border: 1px solid var(--color-border);
    
      /* Input */
      --input-height: 40px;
      --input-padding: var(--space-2-5) var(--space-3);
      --input-radius: var(--radius-md);
      --input-border: 1px solid var(--color-border);
      --input-bg: var(--color-bg-elevated);
    
      /* Nav */
      --nav-height: 64px;
      --nav-bg: var(--color-bg-elevated);
      --nav-shadow: var(--shadow-sm);
    }

    6. Theming: Light & Dark Mode

    Membangun sistem tema yang fleksibel adalah salah satu use case terbaik untuk CSS Variables. Kita akan membangun dark mode yang proper dengan transisi halus.

    Theme System Lengkap

    /* ============================================
       THEME SYSTEM
       Menggunakan data-theme attribute pada html
       ============================================ */
    
    /* Base (Light Theme) */
    :root,
    [data-theme="light"] {
      /* Backgrounds */
      --bg-primary: #ffffff;
      --bg-secondary: #f8fafc;
      --bg-tertiary: #f1f5f9;
      --bg-elevated: #ffffff;
      --bg-overlay: rgba(0, 0, 0, 0.5);
    
      /* Text */
      --text-primary: #0f172a;
      --text-secondary: #475569;
      --text-tertiary: #94a3b8;
      --text-inverse: #ffffff;
      --text-link: #2563eb;
      --text-link-hover: #1d4ed8;
    
      /* Borders */
      --border-primary: #e2e8f0;
      --border-secondary: #cbd5e1;
      --border-focus: #3b82f6;
    
      /* Interactive */
      --interactive-primary: #3b82f6;
      --interactive-primary-hover: #2563eb;
      --interactive-secondary: #f1f5f9;
      --interactive-secondary-hover: #e2e8f0;
    
      /* Status */
      --status-success: #10b981;
      --status-success-bg: #ecfdf5;
      --status-warning: #f59e0b;
      --status-warning-bg: #fffbeb;
      --status-error: #ef4444;
      --status-error-bg: #fef2f2;
      --status-info: #3b82f6;
      --status-info-bg: #eff6ff;
    
      /* Shadows */
      --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.06);
      --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.08);
      --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.08);
      --shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1);
    }
    
    /* Dark Theme */
    [data-theme="dark"] {
      --bg-primary: #0f172a;
      --bg-secondary: #1e293b;
      --bg-tertiary: #334155;
      --bg-elevated: #1e293b;
      --bg-overlay: rgba(0, 0, 0, 0.7);
    
      --text-primary: #f1f5f9;
      --text-secondary: #94a3b8;
      --text-tertiary: #64748b;
      --text-inverse: #0f172a;
      --text-link: #60a5fa;
      --text-link-hover: #93bbfd;
    
      --border-primary: #334155;
      --border-secondary: #475569;
      --border-focus: #60a5fa;
    
      --interactive-primary: #60a5fa;
      --interactive-primary-hover: #93c5fd;
      --interactive-secondary: #334155;
      --interactive-secondary-hover: #475569;
    
      --status-success: #34d399;
      --status-success-bg: #064e3b;
      --status-warning: #fbbf24;
      --status-warning-bg: #78350f;
      --status-error: #f87171;
      --status-error-bg: #7f1d1d;
      --status-info: #60a5fa;
      --status-info-bg: #1e3a8a;
    
      --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3);
      --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.4);
      --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.4);
      --shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.5);
    }
    
    /* Transisi tema yang halus */
    *,
    *::before,
    *::after {
      transition: background-color 0.3s ease,
                  color 0.3s ease,
                  border-color 0.3s ease,
                  box-shadow 0.3s ease;
    }
    
    /* Menggunakan tema di komponen */
    body {
      background-color: var(--bg-primary);
      color: var(--text-primary);
    }
    
    .card {
      background: var(--bg-elevated);
      border: 1px solid var(--border-primary);
      box-shadow: var(--shadow-md);
      color: var(--text-primary);
    }
    
    .alert-success {
      background: var(--status-success-bg);
      color: var(--status-success);
      border: 1px solid var(--status-success);
    }
    
    a {
      color: var(--text-link);
    }
    a:hover {
      color: var(--text-link-hover);
    }

    Auto Dark Mode dengan System Preference

    /* Menggunakan prefers-color-scheme */
    @media (prefers-color-scheme: dark) {
      :root:not([data-theme="light"]) {
        /* Terapkan dark theme kecuali user memilih light */
        --bg-primary: #0f172a;
        --bg-secondary: #1e293b;
        --text-primary: #f1f5f9;
        /* ... (sama seperti [data-theme="dark"]) */
      }
    }
    
    /* Toggle button */
    <button id="themeToggle" aria-label="Ganti tema">
      <span class="theme-icon-light">☀️</span>
      <span class="theme-icon-dark">🌙</span>
    </button>
    
    <style>
    [data-theme="light"] .theme-icon-dark { display: none; }
    [data-theme="dark"] .theme-icon-light { display: none; }
    </style>
    
    <script>
    const html = document.documentElement;
    const toggle = document.getElementById('themeToggle');
    
    // Inisialisasi
    const saved = localStorage.getItem('theme');
    if (saved) {
      html.setAttribute('data-theme', saved);
    } else {
      const prefersDark = matchMedia('(prefers-color-scheme: dark)').matches;
      html.setAttribute('data-theme', prefersDark ? 'dark' : 'light');
    }
    
    toggle.addEventListener('click', () => {
      const current = html.getAttribute('data-theme');
      const next = current === 'dark' ? 'light' : 'dark';
      html.setAttribute('data-theme', next);
      localStorage.setItem('theme', next);
    });
    </script>

    Multi-Theme (Brand Themes)

    /* Theme: Brand Blue */
    [data-theme="brand-blue"] {
      --primary-hue: 220;
      --primary-sat: 90%;
      --primary-lum: 55%;
    }
    
    /* Theme: Brand Green */
    [data-theme="brand-green"] {
      --primary-hue: 160;
      --primary-sat: 80%;
      --primary-lum: 45%;
    }
    
    /* Theme: Brand Purple */
    [data-theme="brand-purple"] {
      --primary-hue: 270;
      --primary-sat: 75%;
      --primary-lum: 55%;
    }
    
    /* Derive semua warna dari satu hue */
    :root {
      --primary: hsl(var(--primary-hue), var(--primary-sat), var(--primary-lum));
      --primary-hover: hsl(var(--primary-hue), var(--primary-sat), calc(var(--primary-lum) - 10%));
      --primary-light: hsl(var(--primary-hue), var(--primary-sat), calc(var(--primary-lum) + 30%));
      --primary-subtle: hsl(var(--primary-hue), var(--primary-sat), calc(var(--primary-lum) + 40%));
    }
    
    .btn-primary {
      background: var(--primary);
      color: white;
    }
    .btn-primary:hover {
      background: var(--primary-hover);
    }

    7. Komponen Reusable dengan Variables

    Button Component System

    /* Button base */
    .btn {
      --btn-bg: var(--interactive-primary);
      --btn-text: white;
      --btn-border: transparent;
      --btn-hover-bg: var(--interactive-primary-hover);
      --btn-height: var(--btn-height-md);
      --btn-padding: var(--btn-padding-md);
      --btn-radius: var(--radius-md);
      --btn-font-size: var(--text-sm);
      --btn-font-weight: var(--weight-medium);
      --btn-shadow: none;
    
      display: inline-flex;
      align-items: center;
      justify-content: center;
      gap: var(--space-2);
      height: var(--btn-height);
      padding: var(--btn-padding);
      background: var(--btn-bg);
      color: var(--btn-text);
      border: 1px solid var(--btn-border);
      border-radius: var(--btn-radius);
      font-size: var(--btn-font-size);
      font-weight: var(--btn-font-weight);
      cursor: pointer;
      transition: all var(--transition-fast);
    }
    
    .btn:hover {
      background: var(--btn-hover-bg);
    }
    
    /* Size variants */
    .btn--sm {
      --btn-height: var(--btn-height-sm);
      --btn-padding: var(--btn-padding-sm);
      --btn-font-size: var(--text-xs);
    }
    
    .btn--lg {
      --btn-height: var(--btn-height-lg);
      --btn-padding: var(--btn-padding-lg);
      --btn-font-size: var(--text-base);
    }
    
    /* Style variants — override variabel! */
    .btn--secondary {
      --btn-bg: var(--interactive-secondary);
      --btn-text: var(--text-primary);
      --btn-hover-bg: var(--interactive-secondary-hover);
    }
    
    .btn--ghost {
      --btn-bg: transparent;
      --btn-text: var(--text-primary);
      --btn-hover-bg: var(--interactive-secondary);
    }
    
    .btn--danger {
      --btn-bg: var(--status-error);
      --btn-hover-bg: #dc2626;
    }
    
    .btn--outline {
      --btn-bg: transparent;
      --btn-text: var(--interactive-primary);
      --btn-border: var(--interactive-primary);
      --btn-hover-bg: var(--interactive-primary);
      --btn-hover-text: white;
    }
    
    /* Penggunaan */
    <button class="btn btn--primary">Primary</button>
    <button class="btn btn--secondary btn--sm">Small Secondary</button>
    <button class="btn btn--danger btn--lg">Large Danger</button>
    <button class="btn btn--outline">Outline</button>

    Card Component System

    .card {
      --card-bg: var(--bg-elevated);
      --card-border: var(--border-primary);
      --card-radius: var(--radius-lg);
      --card-padding: var(--space-6);
      --card-shadow: var(--shadow-md);
      --card-hover-translate: -4px;
    
      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: transform 0.3s ease, box-shadow 0.3s ease;
    }
    
    .card:hover {
      transform: translateY(var(--card-hover-translate));
      box-shadow: var(--shadow-lg);
    }
    
    /* Card variants */
    .card--flat {
      --card-shadow: none;
      --card-hover-translate: 0;
    }
    
    .card--accent {
      --card-border: var(--primary);
      border-top: 3px solid var(--primary);
    }
    
    .card--compact {
      --card-padding: var(--space-4);
    }
    
    .card--elevated {
      --card-shadow: var(--shadow-xl);
    }
    
    /* Card sections */
    .card__header {
      padding-bottom: var(--space-4);
      border-bottom: 1px solid var(--card-border);
      margin-bottom: var(--space-4);
    }
    
    .card__body {
      flex: 1;
    }
    
    .card__footer {
      padding-top: var(--space-4);
      border-top: 1px solid var(--card-border);
      margin-top: var(--space-4);
      display: flex;
      gap: var(--space-3);
      justify-content: flex-end;
    }

    8. Teknik Advanced

    Variabel dengan Type Hints (Houdini)

    /* CSS Houdini: Registered custom properties */
    /* Browser yang mendukung bisa menggunakan animasi */
    CSS.registerProperty({
      name: '--gradient-angle',
      syntax: '<angle>',
      inherits: false,
      initialValue: '0deg'
    });
    
    CSS.registerProperty({
      name: '--gradient-color-1',
      syntax: '<color>',
      inherits: false,
      initialValue: '#3b82f6'
    });
    
    CSS.registerProperty({
      name: '--gradient-color-2',
      syntax: '<color>',
      inherits: false,
      initialValue: '#8b5cf6'
    });
    
    .gradient-animated {
      background: conic-gradient(
        from var(--gradient-angle),
        var(--gradient-color-1),
        var(--gradient-color-2),
        var(--gradient-color-1)
      );
      animation: rotateGradient 3s linear infinite;
    }
    
    @keyframes rotateGradient {
      from { --gradient-angle: 0deg; }
      to { --gradient-angle: 360deg; }
    }

    Variabel untuk Responsive Spacing

    /* Fluid spacing system */
    :root {
      /* Menggunakan clamp() untuk spacing responsif */
      --space-fluid-xs: clamp(4px, 0.5vw, 8px);
      --space-fluid-sm: clamp(8px, 1vw, 16px);
      --space-fluid-md: clamp(16px, 2vw, 32px);
      --space-fluid-lg: clamp(32px, 4vw, 64px);
      --space-fluid-xl: clamp(48px, 6vw, 96px);
    
      /* Fluid typography */
      --text-fluid-sm: clamp(0.875rem, 1vw + 0.5rem, 1rem);
      --text-fluid-base: clamp(1rem, 1.2vw + 0.5rem, 1.125rem);
      --text-fluid-lg: clamp(1.25rem, 2vw + 0.5rem, 1.75rem);
      --text-fluid-xl: clamp(1.5rem, 3vw + 0.5rem, 2.5rem);
      --text-fluid-2xl: clamp(2rem, 4vw + 0.5rem, 3.5rem);
      --text-fluid-3xl: clamp(2.5rem, 5vw + 0.5rem, 5rem);
    }
    
    .section {
      padding: var(--space-fluid-xl) var(--space-fluid-md);
    }
    
    .section h2 {
      font-size: var(--text-fluid-2xl);
      margin-bottom: var(--space-fluid-md);
    }

    Variabel untuk Grid Layout

    /* Grid system dengan variabel */
    .grid {
      --grid-cols: 12;
      --grid-gap: var(--space-4);
    
      display: grid;
      grid-template-columns: repeat(var(--grid-cols), 1fr);
      gap: var(--grid-gap);
    }
    
    /* Span helpers */
    .col-1 { grid-column: span 1; }
    .col-2 { grid-column: span 2; }
    .col-3 { grid-column: span 3; }
    .col-4 { grid-column: span 4; }
    .col-5 { grid-column: span 5; }
    .col-6 { grid-column: span 6; }
    .col-7 { grid-column: span 7; }
    .col-8 { grid-column: span 8; }
    .col-9 { grid-column: span 9; }
    .col-10 { grid-column: span 10; }
    .col-11 { grid-column: span 11; }
    .col-12 { grid-column: span 12; }
    
    /* Responsive: ubah variabel */
    @media (max-width: 768px) {
      .grid {
        --grid-cols: 4;
        --grid-gap: var(--space-3);
      }
      .col-md-4 { grid-column: span 4; }
    }
    
    @media (max-width: 480px) {
      .grid {
        --grid-cols: 2;
      }
      .col-sm-2 { grid-column: span 2; }
    }

    9. Best Practices

    ✅ Best Practices CSS Variables
    • 📌 Nama yang bermakna: Gunakan prefix seperti --color-, --space-, --font-
    • 📌 3 Layer tokens: Primitive → Semantic → Component
    • 📌 Selalu berikan fallback: var(--color, #333)
    • 📌 Gunakan :root untuk global, komponen spesifik untuk local
    • 📌 Hindari deep nesting: var(--a, var(--b, var(--c, fallback)))
    • 📌 Konsisten dengan unit: Simpan angka saja, gunakan calc()
    • 📌 Dokumentasikan: Buat style guide yang menunjukkan semua tokens
    • 📌 Jangan over-engineer: Variabel hanya untuk nilai yang benar-benar berubah
    ⚠️ Yang Perlu Diingat
    • ❌ CSS Variables tidak bisa digunakan dalam @media query sebagai nilai breakpoint
    • ❌ CSS Variables tidak bisa digunakan sebagai nama property (var(--prop): value tidak valid)
    • ❌ CSS Variables tidak mempengaruhi specificity — specificity ditentukan oleh selector
    • ⚠️ Animasi CSS Variables hanya bekerja dengan CSS.registerProperty() (Houdini)

    10. Quiz Pemahaman

    Uji pemahamanmu tentang CSS Variables & Design System:

    Pertanyaan 1: Apa keuntungan utama CSS Variables dibanding Sass Variables?

    a) Lebih cepat di-compile
    b) Bisa diubah secara dinamis di browser (runtime)
    c) Nama variabel lebih pendek
    d) Tidak butuh preprocessing

    Pertanyaan 2: Kode color: var(--warna, red) — apa fungsi dari red?

    a) Warna alternatif saat hover
    b) Warna default yang digunakan jika --warna tidak didefinisikan
    c) Warna untuk dark mode
    d) Warna parent element

    Pertanyaan 3: Bagaimana cara mengubah CSS Variable via JavaScript?

    a) document.style.setVariable('--x', 'value')
    b) document.documentElement.style.setProperty('--x', 'value')
    c) CSS.setVariable('--x', 'value')
    d) document.style['--x'] = 'value'

    Pertanyaan 4: Apa tiga layer dalam design token system?

    a) Base, Layer, Top
    b) Primitive (mentah), Semantic (bermakna), Component (spesifik)
    c) Color, Spacing, Typography
    d) Global, Local, Inline

    Pertanyaan 5: Mana yang BUKAN batasan CSS Variables?

    a) Tidak bisa digunakan di @media query sebagai breakpoint
    b) Tidak bisa diubah secara dinamis
    c) Tidak bisa di-animasi tanpa Houdini
    d) Tidak bisa digunakan sebagai nama property
    🔍 Zoom
    100%
    🎨 Tema