Hướng dẫn Thực hành

Tạo Chủ Đề Màu Sắc Trong React: CSS Variables và Context

Đọc 5 phút

Tạo chủ đề là một trong những vấn đề trông đơn giản cho đến khi bạn phải duy trì nó ở quy mô lớn. Một chuyển đổi đơn giữa chế độ sáng và tối thì đơn giản. Một sản phẩm phục vụ nhiều thương hiệu, cung cấp bảng màu có thể tùy chỉnh bởi người dùng, hoặc cần chuyển đổi chủ đề ngay lập tức mà không tải lại trang đòi hỏi một kiến trúc có chủ đích hơn.

Mô hình component của React và CSS custom properties là sự kết hợp tự nhiên để tạo chủ đề. CSS variables xử lý các giá trị màu một cách khai báo; React Context quản lý trạng thái chủ đề; và Tailwind CSS (trong các dự án hiện đại) kết nối cả hai. Hướng dẫn này đề cập đến từng lớp — cách cấu trúc kiến trúc theme, triển khai nền tảng CSS variable, kết nối React state và xử lý các tình huống đa thương hiệu.


Các Mẫu Kiến Trúc Theme

Mẫu 1: Chỉ CSS Variables (Không Có JavaScript State)

Mẫu đơn giản nhất để hỗ trợ chế độ tối cơ bản không yêu cầu bất kỳ React state nào. Media query prefers-color-scheme thay đổi các giá trị thuộc tính tùy chỉnh và mọi component tự động cập nhật:

/* globals.css */
:root {
  --color-bg: #F8FAFC;
  --color-text: #1E293B;
  --color-brand: #2563EB;
}

@media (prefers-color-scheme: dark) {
  :root {
    --color-bg: #0F172A;
    --color-text: #F1F5F9;
    --color-brand: #60A5FA;
  }
}

Các component sử dụng var(--color-bg) và không bao giờ cần biết về chủ đề hiện tại. Trình duyệt xử lý tất cả.

Khi nào sử dụng: Các site muốn tôn trọng tùy chọn cấp hệ điều hành và không cần nút chuyển đổi do người dùng kiểm soát.

Mẫu 2: Chuyển Đổi Theme Bằng Data Attribute

Thêm thuộc tính data-theme trên phần tử <html> để làm cho chủ đề hoạt động trở nên rõ ràng và có thể ghi đè:

[data-theme="light"],
:root {
  --color-bg: #F8FAFC;
  --color-text: #1E293B;
  --color-brand: #2563EB;
  --color-surface: #FFFFFF;
  --color-border: #E2E8F0;
}

[data-theme="dark"] {
  --color-bg: #0F172A;
  --color-text: #F1F5F9;
  --color-brand: #60A5FA;
  --color-surface: #1E293B;
  --color-border: #334155;
}
const savedTheme = localStorage.getItem('theme') || 'light';
document.documentElement.setAttribute('data-theme', savedTheme);

Khi nào sử dụng: Hầu hết các ứng dụng sản xuất cần nút chuyển đổi do người dùng kiểm soát.

Mẫu 3: Chuyển Đổi Theme Dựa Trên Class

Tương tự data-attribute nhưng sử dụng các CSS class. Cấu hình darkMode: 'class' của Tailwind dựa vào mẫu này:

.dark {
  --color-bg: #0F172A;
  --color-text: #F1F5F9;
}

Khi nào sử dụng: Các dự án sử dụng Tailwind CSS với chiến lược chế độ tối class.


CSS Variables Cho Màu Sắc: Hệ Thống Token

Đặt Tên Theo Ngữ Nghĩa Thay Vì Mô Tả

Việc đặt tên theo giá trị màu (--blue-500) làm cho biến dễ hiểu khi đứng một mình nhưng không thể tạo chủ đề. Đặt tên theo vai trò ngữ nghĩa (--color-brand) cho phép các giá trị thay đổi hoàn toàn qua các chủ đề:

:root {
  --blue-500: #3B82F6;
  --blue-700: #1D4ED8;
  --slate-50: #F8FAFC;
  --slate-800: #1E293B;

  --text-primary: var(--slate-800);
  --text-secondary: #64748B;
  --bg-base: var(--slate-50);
  --bg-elevated: #FFFFFF;
  --brand: var(--blue-700);
  --brand-hover: #1E40AF;
  --error: #DC2626;
  --success: #16A34A;
}

[data-theme="dark"] {
  --text-primary: #F1F5F9;
  --text-secondary: #94A3B8;
  --bg-base: #0F172A;
  --bg-elevated: #1E293B;
  --brand: #60A5FA;
  --brand-hover: #93C5FD;
  --error: #F87171;
  --success: #4ADE80;
}

Tạo Thang Màu

Sử dụng Trình Tạo Sắc Độ để tạo thang 50–950 đầy đủ từ một màu thương hiệu duy nhất.


React Context Cho Trạng Thái Theme

Mẫu ThemeContext

// contexts/ThemeContext.tsx
import { createContext, useContext, useEffect, useState } from 'react';

type Theme = 'light' | 'dark' | 'system';

interface ThemeContextValue {
  theme: Theme;
  resolvedTheme: 'light' | 'dark';
  setTheme: (theme: Theme) => void;
}

const ThemeContext = createContext<ThemeContextValue | undefined>(undefined);

export function ThemeProvider({ children }: { children: React.ReactNode }) {
  const [theme, setThemeState] = useState<Theme>(() => {
    if (typeof window === 'undefined') return 'system';
    return (localStorage.getItem('theme') as Theme) || 'system';
  });

  const systemPrefersDark = typeof window !== 'undefined'
    ? window.matchMedia('(prefers-color-scheme: dark)').matches
    : false;

  const resolvedTheme: 'light' | 'dark' =
    theme === 'system' ? (systemPrefersDark ? 'dark' : 'light') : theme;

  useEffect(() => {
    document.documentElement.setAttribute('data-theme', resolvedTheme);
    localStorage.setItem('theme', theme);
  }, [theme, resolvedTheme]);

  return (
    <ThemeContext.Provider value={{ theme, resolvedTheme, setTheme: setThemeState }}>
      {children}
    </ThemeContext.Provider>
  );
}

export function useTheme(): ThemeContextValue {
  const ctx = useContext(ThemeContext);
  if (!ctx) throw new Error('useTheme must be used within ThemeProvider');
  return ctx;
}

Ngăn Chặn Flash Theme Sai (FOTWT)

Thêm script inline chặn trong <head> đọc localStorage và áp dụng thuộc tính theme trước khi trang render:

<script>
  (function() {
    try {
      var stored = localStorage.getItem('theme');
      var systemDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
      var theme = stored === 'dark' || stored === 'light'
        ? stored
        : (systemDark ? 'dark' : 'light');
      document.documentElement.setAttribute('data-theme', theme);
    } catch(e) {}
  })();
</script>

Tailwind CSS Themes

Tailwind v3: Chế Độ Tối Dựa Trên Class

// tailwind.config.js
module.exports = {
  darkMode: 'class',
  theme: {
    extend: {
      colors: {
        brand: {
          50: '#EFF6FF',
          500: '#3B82F6',
          600: '#2563EB',
          700: '#1D4ED8',
          950: '#172554',
        },
      },
    },
  },
};
<div className="bg-white dark:bg-slate-900 text-slate-800 dark:text-slate-100">
  <button className="bg-brand-600 dark:bg-brand-500 text-white">
    Hành Động Chính
  </button>
</div>

Tailwind v4: Cấu Hình CSS-First

@import "tailwindcss";

@theme {
  --color-brand-50: #EFF6FF;
  --color-brand-500: #3B82F6;
  --color-brand-600: #2563EB;
  --color-brand-700: #1D4ED8;
}

Chiến Lược Đa Thương Hiệu

Thách Thức

Một nền tảng SaaS phục vụ nhiều khách hàng, sản phẩm white-label hoặc hệ thống thiết kế được chia sẻ qua nhiều thương hiệu sản phẩm cần xử lý không chỉ các biến thể tối/sáng mà còn các bản sắc màu hoàn toàn khác nhau.

Brand Tokens Như CSS Variable Overrides

:root {
  --brand-primary-raw: 37 99 235;
  --brand-accent-raw: 99 102 241;
  --brand-primary: rgb(var(--brand-primary-raw));
  --brand-primary-hover: color-mix(in srgb, rgb(var(--brand-primary-raw)) 80%, black);
}

[data-brand="acme"] {
  --brand-primary-raw: 37 99 235;
  --brand-accent-raw: 16 185 129;
}

[data-brand="globex"] {
  --brand-primary-raw: 124 58 237;
  --brand-accent-raw: 245 158 11;
}

Tạo Thang Thương Hiệu Với Shade Generator

Sử dụng Trình Tạo Sắc Độ để tạo thang 50–950 cho màu chính và nhấn của từng thương hiệu.


Những Điểm Chính

  • CSS custom properties là nền tảng: Định nghĩa semantic tokens (--text-primary, --brand-primary) và thay đổi giá trị của chúng theo từng chủ đề.
  • Chuyển đổi theme bằng data-attribute (data-theme="dark") là mẫu linh hoạt nhất.
  • Hệ thống token hai lớp (thang màu nguyên thủy + semantic role tokens) cho phép linh hoạt chủ đề hoàn toàn trong khi giữ cho các component sạch sẽ.
  • Sử dụng Trình Tạo Sắc Độ để tạo thang màu đầy đủ 50–950.
  • React Context quản lý trạng thái theme; các component không bao giờ cần đọc context để áp dụng màu đúng.
  • Ngăn chặn flash theme sai bằng script inline chặn trong <head>.
  • Đa thương hiệu xếp chồng thêm một data attribute (data-brand) lên hệ thống theme.

Màu sắc liên quan

Thương hiệu liên quan

Công cụ liên quan