ข้ามไปเนื้อหาหลัก

Category: reference

CSS Custom Properties — Pattern สำหรับ Theming และ Dark Mode

วิธีใช้ CSS custom properties (variables) อย่างเป็นระบบ สำหรับ theming, dark mode, และ design tokens บน static site

· อ่านประมาณ 2 นาที

สารบัญ

CSS Custom Properties คืออะไร

CSS custom properties (หรือ CSS variables) คือ property ที่ขึ้นต้นด้วย -- ประกาศค่าไว้ที่หนึ่งแล้วใช้ซ้ำได้ทั่ว stylesheet ด้วย var()

:root {
  --color-primary: #2563eb;
  --spacing-base: 1rem;
}

.btn {
  background: var(--color-primary);
  padding: var(--spacing-base);
}

ต่างจาก Sass/Less Variables อย่างไร

CSS Custom PropertiesSass Variables
Runtime✅ เปลี่ยนได้ตอน runtime❌ compile-time เท่านั้น
Inheritance✅ inherit ตาม DOM❌ global scope
JavaScript✅ อ่าน/เขียนได้❌ ไม่มีใน browser
Browser✅ native❌ ต้อง build

Dark Mode Pattern ที่ใช้งานจริง

ใช้ [data-theme] attribute บน <html> แทน @media (prefers-color-scheme) เพราะ override ได้ด้วย user preference:

:root {
  --bg: #f8fafc;
  --text: #0f172a;
  --muted: #64748b;
  --border: rgba(15, 23, 42, 0.1);
  --card-bg: #ffffff;
}

[data-theme='dark'] {
  --bg: #0f172a;
  --text: #e2e8f0;
  --muted: #94a3b8;
  --border: rgba(255, 255, 255, 0.1);
  --card-bg: rgba(255, 255, 255, 0.04);
}

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

การเปลี่ยน theme ทำได้ด้วย JavaScript บรรทัดเดียว:

document.documentElement.dataset.theme = 'dark';

Anti-FOUC (Flash of Unstyled Content)

เมื่อ theme ถูกเก็บใน localStorage ต้องอ่านก่อน render ด้วย inline script ใน <head>:

<head>
  <!-- ต้องเป็น script แรกใน head — ก่อน CSS -->
  <script>
    (function() {
      try {
        var s = localStorage.getItem('theme');
        var dark = window.matchMedia('(prefers-color-scheme: dark)').matches;
        document.documentElement.dataset.theme = s || (dark ? 'dark' : 'light');
      } catch {}
    })();
  </script>
  <link rel="stylesheet" href="/styles.css" />
</head>

ถ้าใส่ script หลัง CSS link จะเห็น flash ขาว/ดำก่อน theme ถูก apply

Scoped Variables

กำหนด variable เฉพาะ component ได้โดยไม่กระทบ global:

.card {
  --card-radius: 12px;
  --card-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
  border-radius: var(--card-radius);
  box-shadow: var(--card-shadow);
}

/* override เฉพาะ card ใน hero section */
.hero .card {
  --card-radius: 20px;
}

Fallback Values

/* ถ้า --color-accent ไม่ถูกประกาศ ใช้ #2563eb แทน */
color: var(--color-accent, #2563eb);

/* ซ้อน fallback ได้ */
color: var(--text-primary, var(--text, #0f172a));

calc() กับ Custom Properties

:root {
  --base-size: 4px;
}

.spacing-sm { margin: calc(var(--base-size) * 2); }  /* 8px */
.spacing-md { margin: calc(var(--base-size) * 4); }  /* 16px */
.spacing-lg { margin: calc(var(--base-size) * 8); }  /* 32px */

อ่าน/เขียนจาก JavaScript

// อ่านค่า
const primary = getComputedStyle(document.documentElement)
  .getPropertyValue('--color-primary').trim();

// เขียนค่า (เช่น progress bar)
document.documentElement.style.setProperty('--progress', `${pct}%`);
#progress-bar {
  width: var(--progress, 0%);
  transition: width 0.1s linear;
}

Design Token Pattern

จัดระดับ token เป็น 3 ชั้น:

/* 1. Primitive — ค่าดิบ */
:root {
  --blue-500: #3b82f6;
  --blue-600: #2563eb;
  --gray-50: #f8fafc;
}

/* 2. Semantic — ความหมาย */
:root {
  --color-primary: var(--blue-600);
  --color-bg: var(--gray-50);
}

/* 3. Component — เฉพาะ element */
.btn-primary {
  background: var(--color-primary);
}

เมื่อเปลี่ยน theme ปรับแค่ชั้น Semantic — ชั้น Component ไม่ต้องแตะ