Category: reference
CSS Custom Properties — Pattern สำหรับ Theming และ Dark Mode
วิธีใช้ CSS custom properties (variables) อย่างเป็นระบบ สำหรับ theming, dark mode, และ design tokens บน static site
สารบัญ
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 Properties | Sass 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 ไม่ต้องแตะ