Memuat...
👋 Selamat Pagi!

Panduan CSS Custom Properties untuk Tema Dinamis di Web App

Kuasai CSS custom properties untuk bangun sistem tema dinamis tanpa JavaScript. Panduan lengkap dark mode, design tokens, dan theming profesional.

Panduan CSS Custom Properties untuk Tema Dinamis di Web App

Bayangkan kamu bisa mengganti seluruh tampilan aplikasi web — warna, spacing, typography — hanya dengan mengubah beberapa baris CSS.

Tidak perlu rebuild. Tidak perlu JavaScript. Tidak perlu preprocessor seperti SASS.

Inilah kekuatan nyata dari CSS Custom Properties, atau yang sering disebut CSS variables. Fitur ini bukan sekadar cara lain menyimpan nilai warna. Ia adalah fondasi dari sistem desain modern yang fleksibel, dinamis, dan mudah di-maintain.

Artikel ini akan membahas tuntas bagaimana membangun sistem tema yang sesungguhnya menggunakan CSS custom properties, mulai dari design tokens, dark mode tanpa JavaScript, hingga integrasi dengan JavaScript untuk theming dinamis penuh.

Apa Itu CSS Custom Properties dan Kenapa Lebih Powerful dari Preprocessor

CSS Custom Properties adalah variabel yang didefinisikan langsung di dalam CSS dan bisa diakses oleh browser secara native.

Cara mendefinisikannya sangat sederhana:

:root {
  --color-primary: #3b82f6;
  --spacing-base: 16px;
  --font-size-body: 1rem;
}

Kemudian digunakan dengan fungsi var():

.button {
  background-color: var(--color-primary);
  padding: var(--spacing-base);
  font-size: var(--font-size-body);
}

Sekilas mirip dengan variabel di SASS atau LESS. Tapi perbedaannya sangat fundamental.

Variabel preprocessor bersifat statis. Mereka dikompilasi sebelum kode sampai ke browser. Setelah CSS digenerate, nilainya tidak bisa berubah tanpa kompilasi ulang.

CSS Custom Properties bersifat live. Browser membacanya secara langsung, dan nilainya bisa berubah saat runtime — via CSS itu sendiri, atau via JavaScript.

Perbedaan lain yang krusial: CSS custom properties mengikuti aturan cascade dan inheritance. Artinya, kamu bisa mendefinisikan ulang nilai variabel di selector tertentu, dan semua elemen di dalamnya akan otomatis menyesuaikan.

/* Default */
:root {
  --color-surface: #ffffff;
  --color-text: #1a1a1a;
}

/* Override di dalam komponen card */
.card-dark {
  --color-surface: #1e1e2e;
  --color-text: #cdd6f4;
}

Semua elemen di dalam .card-dark akan otomatis menggunakan warna yang telah di-override, tanpa perlu menulis ulang setiap property satu per satu.

Membangun Sistem Token Warna dan Spacing dengan CSS Variables

Sebelum bicara tentang tema, kita perlu memahami konsep design token.

Design token adalah unit terkecil dari keputusan desain — warna, spacing, typography, shadow, border radius, dll — yang disimpan sebagai variabel yang bisa digunakan secara konsisten di seluruh aplikasi.

Pendekatan terbaik adalah membagi token ke dalam dua lapisan:

Lapisan 1: Primitive Tokens Ini adalah nilai mentah yang tidak terikat makna semantik.

:root {
  /* Color Palette */
  --blue-50: #eff6ff;
  --blue-100: #dbeafe;
  --blue-500: #3b82f6;
  --blue-600: #2563eb;
  --blue-900: #1e3a5f;

  --neutral-50: #f9fafb;
  --neutral-100: #f3f4f6;
  --neutral-700: #374151;
  --neutral-900: #111827;

  /* Spacing Scale */
  --space-1: 4px;
  --space-2: 8px;
  --space-3: 12px;
  --space-4: 16px;
  --space-6: 24px;
  --space-8: 32px;
  --space-12: 48px;

  /* Typography */
  --text-sm: 0.875rem;
  --text-base: 1rem;
  --text-lg: 1.125rem;
  --text-xl: 1.25rem;
  --text-2xl: 1.5rem;

  /* Border Radius */
  --radius-sm: 4px;
  --radius-md: 8px;
  --radius-lg: 16px;
  --radius-full: 9999px;
}

Lapisan 2: Semantic Tokens Ini adalah token yang memiliki makna kontekstual. Nilainya mengacu pada primitive tokens.

:root {
  /* Brand Colors */
  --color-primary: var(--blue-500);
  --color-primary-hover: var(--blue-600);
  --color-primary-light: var(--blue-50);

  /* Surface & Text */
  --color-background: var(--neutral-50);
  --color-surface: #ffffff;
  --color-text-primary: var(--neutral-900);
  --color-text-secondary: var(--neutral-700);
  --color-border: var(--neutral-100);

  /* Semantic Spacing */
  --spacing-component: var(--space-4);
  --spacing-section: var(--space-12);
  --spacing-card-padding: var(--space-6);
}

Kenapa dua lapisan? Karena ketika kamu mengganti tema, yang berubah hanyalah semantic tokens. Primitive tokens tetap sama. Ini membuat sistem tema jauh lebih predictable dan mudah dikelola.

Teknik Dark Mode Tanpa JavaScript Menggunakan Custom Properties

Ini adalah salah satu trik paling elegan yang bisa kamu lakukan dengan CSS Custom Properties.

Dark mode bisa diimplementasikan hanya dengan CSS, memanfaatkan media query prefers-color-scheme dan cascade.

/* Light Mode (default) */
:root {
  --color-background: #f9fafb;
  --color-surface: #ffffff;
  --color-text-primary: #111827;
  --color-text-secondary: #374151;
  --color-border: #e5e7eb;
  --color-primary: #3b82f6;
  --color-primary-text: #ffffff;

  /* Shadow */
  --shadow-card: 0 1px 3px rgba(0, 0, 0, 0.1);
}

/* Dark Mode otomatis berdasarkan preferensi OS */
@media (prefers-color-scheme: dark) {
  :root {
    --color-background: #0f172a;
    --color-surface: #1e293b;
    --color-text-primary: #f1f5f9;
    --color-text-secondary: #94a3b8;
    --color-border: #334155;
    --color-primary: #60a5fa;
    --color-primary-text: #0f172a;

    --shadow-card: 0 1px 3px rgba(0, 0, 0, 0.4);
  }
}

Dengan pendekatan ini, semua komponen yang menggunakan semantic tokens akan otomatis menyesuaikan tampilannya saat pengguna mengaktifkan dark mode di sistem operasi mereka.

/* Komponen Card — tidak perlu media query di sini */
.card {
  background-color: var(--color-surface);
  color: var(--color-text-primary);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  padding: var(--spacing-card-padding);
  box-shadow: var(--shadow-card);
}

.card h2 {
  color: var(--color-text-primary);
  font-size: var(--text-xl);
}

.card p {
  color: var(--color-text-secondary);
  font-size: var(--text-base);
}

Menambahkan Toggle Manual

Terkadang pengguna ingin memilih tema secara manual, terlepas dari pengaturan OS mereka.

Strateginya sederhana: tambahkan atribut data-theme pada elemen html, lalu override variabel berdasarkan atribut tersebut.

/* Override manual untuk dark mode */
[data-theme="dark"] {
  --color-background: #0f172a;
  --color-surface: #1e293b;
  --color-text-primary: #f1f5f9;
  --color-text-secondary: #94a3b8;
  --color-border: #334155;
}

/* Override manual untuk light mode */
[data-theme="light"] {
  --color-background: #f9fafb;
  --color-surface: #ffffff;
  --color-text-primary: #111827;
  --color-text-secondary: #374151;
  --color-border: #e5e7eb;
}

Sekarang kamu hanya perlu mengubah satu atribut HTML untuk mengganti tema seluruh halaman.

Menghubungkan CSS Variables dengan JavaScript untuk Tema Dinamis

Untuk kasus yang lebih advanced — misalnya tema kustom pilihan pengguna — kita perlu menghubungkan CSS Custom Properties dengan JavaScript.

Membaca dan Menulis CSS Variables via JavaScript

// Membaca nilai CSS variable
const root = document.documentElement;
const primaryColor = getComputedStyle(root)
  .getPropertyValue('--color-primary')
  .trim();

console.log(primaryColor); // "#3b82f6"

// Mengubah nilai CSS variable
root.style.setProperty('--color-primary', '#10b981');
root.style.setProperty('--color-primary-hover', '#059669');

Perubahan ini langsung terlihat di semua elemen yang menggunakan --color-primary, secara instan, tanpa perlu reload halaman.

Sistem Toggle Dark Mode dengan LocalStorage

class ThemeManager {
  constructor() {
    this.root = document.documentElement;
    this.storageKey = 'app-theme';
    this.init();
  }

  init() {
    // Cek preferensi yang sudah tersimpan
    const savedTheme = localStorage.getItem(this.storageKey);

    if (savedTheme) {
      this.setTheme(savedTheme);
    } else {
      // Ikuti preferensi OS
      const prefersDark = window.matchMedia(
        '(prefers-color-scheme: dark)'
      ).matches;
      this.setTheme(prefersDark ? 'dark' : 'light');
    }

    // Pantau perubahan preferensi OS
    window.matchMedia('(prefers-color-scheme: dark)')
      .addEventListener('change', (e) => {
        if (!localStorage.getItem(this.storageKey)) {
          this.setTheme(e.matches ? 'dark' : 'light');
        }
      });
  }

  setTheme(theme) {
    this.root.setAttribute('data-theme', theme);
    localStorage.setItem(this.storageKey, theme);
  }

  toggle() {
    const current = this.root.getAttribute('data-theme');
    this.setTheme(current === 'dark' ? 'light' : 'dark');
  }
}

// Inisialisasi
const themeManager = new ThemeManager();

// Tombol toggle
document.getElementById('theme-toggle')
  .addEventListener('click', () => themeManager.toggle());

Tema Warna Kustom dari Input Pengguna

Ini adalah level berikutnya: membiarkan pengguna memilih warna brand mereka sendiri.

function applyCustomTheme(primaryHex) {
  const root = document.documentElement;

  // Set warna utama
  root.style.setProperty('--color-primary', primaryHex);

  // Hitung warna hover (sedikit lebih gelap)
  // Untuk produksi, gunakan library chroma.js atau color2k
  const darkerColor = adjustBrightness(primaryHex, -15);
  root.style.setProperty('--color-primary-hover', darkerColor);

  // Simpan ke localStorage
  localStorage.setItem('custom-primary', primaryHex);
}

// Contoh penggunaan dengan color picker
document.getElementById('color-picker')
  .addEventListener('input', (e) => {
    applyCustomTheme(e.target.value);
  });

Kesulitan dengan tugas programming atau butuh bantuan coding? KerjaKode siap membantu menyelesaikan tugas IT dan teknik informatika Anda. Dapatkan bantuan profesional di jasa tugas IT KerjaKode.

Studi Kasus: Sistem Tema untuk Dashboard Admin Indonesia

Mari kita lihat implementasi nyata untuk konteks lokal.

Bayangkan kamu membangun dashboard admin untuk sebuah platform UMKM Indonesia. Kliennya bermacam-macam — ada yang suka tampilan bersih minimalis, ada yang minta warna merah-putih, ada yang butuh mode gelap untuk operator shift malam.

Struktur Token yang Direkomendasikan

/* design-tokens.css */
:root {
  /* === PRIMITIVE TOKENS === */
  --red-500: #ef4444;
  --red-600: #dc2626;
  --green-500: #22c55e;
  --yellow-500: #eab308;
  --white: #ffffff;
  --black: #000000;

  /* === SEMANTIC TOKENS: DEFAULT THEME === */
  --theme-primary: #2563eb;
  --theme-primary-hover: #1d4ed8;
  --theme-primary-subtle: #eff6ff;

  --theme-bg: #f1f5f9;
  --theme-surface: #ffffff;
  --theme-surface-raised: #f8fafc;

  --theme-text: #0f172a;
  --theme-text-muted: #64748b;
  --theme-text-on-primary: #ffffff;

  --theme-border: #e2e8f0;
  --theme-border-strong: #cbd5e1;

  --theme-success: var(--green-500);
  --theme-warning: var(--yellow-500);
  --theme-danger: var(--red-500);

  /* === COMPONENT TOKENS === */
  --sidebar-bg: var(--theme-primary);
  --sidebar-text: rgba(255, 255, 255, 0.85);
  --sidebar-text-active: var(--white);
  --sidebar-item-active-bg: rgba(255, 255, 255, 0.15);

  --card-bg: var(--theme-surface);
  --card-border: var(--theme-border);
  --card-radius: 12px;
  --card-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);

  --table-header-bg: var(--theme-surface-raised);
  --table-row-hover: var(--theme-primary-subtle);
  --table-border: var(--theme-border);

  --badge-success-bg: #dcfce7;
  --badge-success-text: #166534;
  --badge-warning-bg: #fef9c3;
  --badge-warning-text: #854d0e;
  --badge-danger-bg: #fee2e2;
  --badge-danger-text: #991b1b;
}

/* === DARK THEME === */
[data-theme="dark"] {
  --theme-bg: #0f172a;
  --theme-surface: #1e293b;
  --theme-surface-raised: #263347;
  --theme-text: #f1f5f9;
  --theme-text-muted: #94a3b8;
  --theme-border: #334155;
  --theme-border-strong: #475569;

  --card-shadow: 0 1px 4px rgba(0, 0, 0, 0.3);

  --badge-success-bg: #14532d;
  --badge-success-text: #86efac;
  --badge-warning-bg: #422006;
  --badge-warning-text: #fde047;
  --badge-danger-bg: #450a0a;
  --badge-danger-text: #fca5a5;
}

/* === MERAH PUTIH THEME (untuk branding lokal) === */
[data-theme="merah-putih"] {
  --theme-primary: #cc0001;
  --theme-primary-hover: #a30001;
  --theme-primary-subtle: #fff0f0;
  --sidebar-bg: #cc0001;
  --table-row-hover: #fff0f0;
}

Menggunakan Token di Komponen

/* dashboard.css */
.sidebar {
  background-color: var(--sidebar-bg);
  color: var(--sidebar-text);
  width: 240px;
  height: 100vh;
  padding: var(--space-4);
}

.sidebar-link {
  color: var(--sidebar-text);
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-md);
  display: flex;
  align-items: center;
  gap: var(--space-2);
  transition: background-color 0.15s ease;
}

.sidebar-link:hover,
.sidebar-link.active {
  background-color: var(--sidebar-item-active-bg);
  color: var(--sidebar-text-active);
}

.stat-card {
  background: var(--card-bg);
  border: 1px solid var(--card-border);
  border-radius: var(--card-radius);
  padding: var(--space-6);
  box-shadow: var(--card-shadow);
}

.stat-card .value {
  font-size: var(--text-2xl);
  font-weight: 700;
  color: var(--theme-text);
}

.stat-card .label {
  font-size: var(--text-sm);
  color: var(--theme-text-muted);
}

.badge {
  display: inline-flex;
  align-items: center;
  padding: 2px 10px;
  border-radius: var(--radius-full);
  font-size: var(--text-sm);
  font-weight: 500;
}

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

.badge-warning {
  background-color: var(--badge-warning-bg);
  color: var(--badge-warning-text);
}

JavaScript untuk Theme Switcher Dashboard

// theme-switcher.js
const themes = ['default', 'dark', 'merah-putih'];

function initThemeSwitcher() {
  const saved = localStorage.getItem('dashboard-theme') || 'default';
  applyTheme(saved);

  document.querySelectorAll('[data-set-theme]').forEach((btn) => {
    btn.addEventListener('click', () => {
      const theme = btn.dataset.setTheme;
      applyTheme(theme);
      localStorage.setItem('dashboard-theme', theme);
    });
  });
}

function applyTheme(theme) {
  if (theme === 'default') {
    document.documentElement.removeAttribute('data-theme');
  } else {
    document.documentElement.setAttribute('data-theme', theme);
  }

  // Update UI aktif pada tombol switcher
  document.querySelectorAll('[data-set-theme]').forEach((btn) => {
    btn.classList.toggle('active', btn.dataset.setTheme === theme);
  });
}

document.addEventListener('DOMContentLoaded', initThemeSwitcher);

HTML-nya pun sangat sederhana:

<div class="theme-switcher">
  <button data-set-theme="default" title="Light Theme">☀️ Light</button>
  <button data-set-theme="dark" title="Dark Theme">🌙 Dark</button>
  <button data-set-theme="merah-putih" title="Tema Merah Putih">🇮🇩 Merah Putih</button>
</div>

Tips Praktis dan Hal yang Perlu Dihindari

Beberapa hal penting yang perlu kamu perhatikan saat menggunakan CSS Custom Properties di proyek nyata:

Gunakan fallback value untuk browser lama atau nilai yang mungkin tidak terdefinisi:

.button {
  /* Fallback: #3b82f6 jika variabel tidak terdefinisi */
  background-color: var(--color-primary, #3b82f6);
}

Hindari terlalu banyak lapisan variabel yang saling merujuk. Tiga lapisan biasanya sudah cukup: primitive → semantic → component.

Gunakan naming convention yang konsisten. Pola --kategori-nama-varian seperti --color-text-muted atau --spacing-card-padding jauh lebih readable daripada nama generik seperti --var1 atau --c3.

Jangan override terlalu dalam. Jika kamu mendefinisikan ulang variabel di dalam komponen yang bersarang, kamu bisa menciptakan perilaku yang sulit diprediksi.

CSS Custom Properties bukan fitur baru, tapi sangat banyak developer yang masih menggunakannya sebatas mengganti warna hardcoded. Dengan pendekatan design token yang terstruktur, kamu bisa membangun sistem desain yang benar-benar scalable — yang bisa digunakan oleh tim besar, didukung beberapa tema, dan mudah di-maintain selama bertahun-tahun.

Mulai dari proyek kecil pun sudah cukup. Definisikan token kamu, pisahkan primitive dari semantic, dan biarkan cascade bekerja untuk kamu.

Ajie Kusumadhany
Written by

Ajie Kusumadhany

Founder & Lead Developer KerjaKode. Berpengalaman dalam pengembangan web modern dengan Laravel, React.js, Vue.js, dan teknologi terkini. Passionate tentang coding, teknologi, dan berbagi pengetahuan melalui artikel.

Promo Spesial Hari Ini!

10% DISKON

Promo berakhir dalam:

00 Jam
:
00 Menit
:
00 Detik
Klaim Promo Sekarang!

*Promo berlaku untuk order hari ini

0
User Online
Halo! 👋
Kerjakode Support Online
×

👋 Hai! Pilih layanan yang kamu butuhkan:

Chat WhatsApp Sekarang