การกำหนดธีมสีแบบไดนามิกใน React: เกินกว่า CSS Variables
Embed This Widget
Add the script tag and a data attribute to embed this widget.
Embed via iframe for maximum compatibility.
<iframe src="https://colorfyi.com/iframe/entity//" width="420" height="400" frameborder="0" style="border:0;border-radius:10px;max-width:100%" loading="lazy"></iframe>
Paste this URL in WordPress, Medium, or any oEmbed-compatible platform.
https://colorfyi.com/entity//
Add a dynamic SVG badge to your README or docs.
[](https://colorfyi.com/entity//)
Use the native HTML custom element.
บทช่วยสอนการกำหนดธีมส่วนใหญ่หยุดที่การสลับโหมดมืด สลับคลาสบน <html> พลิก CSS variables สองสามตัว เสร็จสิ้น นั่นครอบคลุมกรณีทั่วไปแต่พลาดปัญหาที่น่าสนใจกว่า: จะทำอย่างไรถ้าผู้ใช้สามารถเลือกสีแบรนด์ของตัวเองได้? จะทำอย่างไรถ้าผลิตภัณฑ์ SaaS ของคุณให้บริการลูกค้าหลายราย แต่ละรายมีเอกลักษณ์ของตัวเอง?
นั่นคือจุดที่การกำหนดธีมสีแบบไดนามิกเริ่มต้น — และ CSS variables เพียงอย่างเดียวไม่เพียงพอ
แนวทาง CSS Variable สำหรับธีม React
พื้นฐาน: สถาปัตยกรรม Semantic Token
ทุกระบบธีมแบบไดนามิกเริ่มต้นด้วยฐานเดียวกัน: CSS custom properties ที่ตั้งชื่อตามบทบาทเชิงความหมาย ไม่ใช่ค่าสี อย่าตั้งชื่อตัวแปรว่า --blue-500 ตั้งชื่อว่า --brand-primary:
/* globals.css */
:root {
--bg-base: #F8FAFC;
--bg-surface: #FFFFFF;
--text-primary: #1E293B;
--text-secondary: #64748B;
--brand-primary: #2563EB;
--brand-hover: #1D4ED8;
--brand-subtle: #EFF6FF;
--on-brand: #FFFFFF;
--border-default: #E2E8F0;
--status-error: #DC2626;
--status-success: #16A34A;
--status-warning: #D97706;
}
คอมโพเนนต์อ้างถึง token ไม่ใช่สีดิบ:
.card {
background: var(--bg-surface);
border: 1px solid var(--border-default);
color: var(--text-primary);
}
การสร้างธีมแบบ Runtime ด้วยอัลกอริทึมสี
การสร้างชุด Token ที่สมบูรณ์
import chroma from 'chroma-js';
function generateThemeTokens(brandHex) {
const brand = chroma(brandHex);
const brandLuminance = brand.luminance();
const onBrand = brandLuminance > 0.179 ? '#000000' : '#FFFFFF';
const scale = chroma.scale([
chroma(brandHex).brighten(2.5).desaturate(0.5).hex(),
brandHex,
chroma(brandHex).darken(2.5).hex(),
]).mode('oklch').colors(11);
return {
'--brand-primary': brandHex,
'--brand-hover': chroma(brandHex).darken(0.5).hex(),
'--brand-subtle': scale[1],
'--on-brand': onBrand,
'--brand-50': scale[0],
'--brand-500': scale[5],
'--brand-950': scale[10],
};
}
React Context สำหรับการจัดการสถานะธีม
สถาปัตยกรรม ThemeContext
// contexts/ThemeContext.tsx
import { createContext, useContext, useEffect, useState, useCallback, type ReactNode } from 'react';
import { generateThemeTokens } from '../lib/theme-generator';
interface ThemeState {
brandHex: string;
mode: 'light' | 'dark' | 'system';
}
const ThemeContext = createContext<any>(undefined);
const STORAGE_KEY = 'app-theme';
export function ThemeProvider({ children }: { children: ReactNode }) {
const [state, setState] = useState<ThemeState>(() => {
try {
const stored = localStorage.getItem(STORAGE_KEY);
if (stored) return JSON.parse(stored);
} catch {}
return { brandHex: '#2563EB', mode: 'system' };
});
useEffect(() => {
const tokens = generateThemeTokens(state.brandHex);
const root = document.documentElement;
Object.entries(tokens).forEach(([prop, value]) => {
root.style.setProperty(prop, value as string);
});
localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
}, [state]);
const setBrand = useCallback((hex: string) => {
setState(prev => ({ ...prev, brandHex: hex }));
}, []);
return (
<ThemeContext.Provider value={{ ...state, setBrand }}>
{children}
</ThemeContext.Provider>
);
}
export function useTheme() {
const ctx = useContext(ThemeContext);
if (!ctx) throw new Error('useTheme must be used within ThemeProvider');
return ctx;
}
คลาสสี Dynamic ของ Tailwind CSS
Tailwind สร้างคลาส utility ในเวลา build วิธีที่สะอาดที่สุดคือการกำหนดชุด token ธีม Tailwind ขนาดเล็กที่ชี้ไปยัง CSS custom properties:
/* styles.css (Tailwind v4) */
@import "tailwindcss";
@theme {
--color-brand: var(--brand-primary);
--color-brand-hover: var(--brand-hover);
--color-on-brand: var(--on-brand);
}
ตอนนี้ bg-brand และ text-on-brand เป็นคลาส Tailwind ที่ถูกต้อง:
<button className="bg-brand text-on-brand hover:bg-brand-hover px-4 py-2 rounded-lg">
การกระทำหลัก
</button>
สำหรับสีที่ไม่ซ้ำกันอย่างแท้จริง ใช้ inline style:
<div style={{ backgroundColor: swatch.hex }} className="w-8 h-8 rounded" />
การป้องกัน Flash ของธีมเริ่มต้นเมื่อโหลด
// app/layout.tsx (Next.js App Router)
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" suppressHydrationWarning>
<head>
<script
dangerouslySetInnerHTML={{
__html: `
(function() {
try {
var stored = JSON.parse(localStorage.getItem('app-theme') || '{}');
var brand = stored.brandHex || '#2563EB';
document.documentElement.style.setProperty('--brand-primary', brand);
} catch(e) {}
})();
`,
}}
/>
</head>
<body><ThemeProvider>{children}</ThemeProvider></body>
</html>
);
}
ประเด็นสำคัญ
- CSS custom properties คือรากฐาน — semantic token ที่ตั้งชื่อตามบทบาท ไม่ใช่ค่าสี ช่วยให้เปลี่ยนธีมได้อย่างสมบูรณ์โดยไม่ต้องแก้ไขคอมโพเนนต์
- การสร้าง token แบบ runtime จาก hex แบรนด์เดียวต้องการ: สเกลความสว่าง/ความมืด การตรวจสอบความเปรียบต่าง WCAG สำหรับข้อความ on-brand และการคำนวณแยกสำหรับตัวแปรโหมดมืด
- React context จัดการสถานะธีมและใช้ token กับ DOM เป็น side effect — คอมโพเนนต์ไม่ต้องสมัครรับ context ธีมเพื่อใช้สี
- Debounce การคำนวณสีที่มีค่าใช้จ่ายสูง (150ms) เมื่อเชื่อมต่อกับ input ต่อเนื่อง
- สำหรับ Tailwind CSS: กำหนด CSS variable bridge ใน
@themeเพื่อให้ utilitybg-brandสะท้อนค่า token ที่ runtime - ใช้ เครื่องมือสร้างเฉดสี และ เครื่องมือสร้างจานสี เพื่อตรวจสอบสเกลสีที่สร้างขึ้น