Design System 101 - 深淺主題 (Theming)

前言

深淺主題已經在前端社群風靡多年,小至個人部落格大至世界級客戶端應用 (例如: Github),都有深淺色的切換功能,提供使用者可以自由地切換主題。

而要讓一個網站可以無痛的切換深淺色,筆者認為最主要的原則就是在開發時遵循 設計標籤 Design Token,也在先前 設計標籤到 CSS 的章節提到過其好處,如果色票沒有列在 Token 內則需要在應用層的 CSS 中去處理。本篇會介紹如何透過 useTheme 去控制應用的深淺主題。

回顧

設計標籤 (Materal Design)

在最初的一開始我們介紹 Token 這個概念,透過 System Token (alias.json) 的深淺色模式分別指向不同的 Ref Token (base.json),這一步讓我們有能力可以讓同一個 System Token 的 alias 根據某些條件轉換成不同的值。

– Source: Materal Design

Normalized CSS

接著再透過設計標籤建置時,將 :root 拆分成 light 與 dark 到 normalized.css (Source Code),並在網頁一開始載入該 CSS ,最後透過 data-theme 去控制要使用哪種主題色。

html[data-theme='light'],
.tocino-light {
--tocino-sys-color-primary: #6750a4;
// ... All System Token (淺色)
}
:root {
--tocino-sys-color-primary: #d0bcff;
// ... All System Token (深色)
}

接下來將會介紹如何實作讓使用者可以切換深淺色模式的一個功能,也就是本篇要介紹的 useTheme

useTheme

描述

useTheme 要做的事情很簡單,就是控制 data-theme 的值。

API

理想上我們希望當使用 useTheme 時可以讓它可以讓我們更新 data-theme 以及知道當前的 theme

import { useTheme } from '@tocino-ui/core/hooks';
export default () => {
const { theme, toggleTheme } = useTheme();
return (
<div>
<span>Current Theme: {theme}</span>
<button onClick={toggleTheme}>Toggle Theme</button>
</div>
);
};

參數

名稱型別初始值描述
initialThemestring-初始的主題

回傳 API

名稱型別初始值描述
themestringdark當前的主題
toggleTheme() => void-切換主題
setThemeSetStateAction-設定主題

實作

useTheme

首先會先透過 getTheme 取得 localStorage 的前次所存取的模式,如果 localStorage 沒有值,就將深色主題作為預設值,並且頁面渲染時透過 useEffect 中去更新 data-theme。最後將 theme, toggleThemesetTheme 這三個 API 作為 useTheme 的回傳值。

themeManager

其主要負責 localStorage 的存取與透過 documentElement.setAttribute 去更新 data-theme 的值。

import React, { useEffect, useState, useCallback, useMemo } from 'react';
import { Button } from '@tocino-ui/button';

/**
 * ====== CONST ======
 */
const THEME_KEY = 'tocino-theme';
const THEME = {
  LIGHT: 'light',
  DARK: 'dark',
  SYSTEM: 'system',
};

/**
 * ====== THEME MANAGER ======
 */

const themeManager = {
  getTheme: () => {
    return window?.localStorage.getItem(THEME_KEY) ?? THEME.DARK;
  },
  setTheme: (theme) => {
    document?.documentElement.setAttribute('data-theme', theme);
    window?.localStorage.setItem(THEME_KEY, theme);
  },
};

/**
 * ====== `useTheme` ======
 */

function useTheme(initialTheme) {
  const [theme, setTheme] = useState(() => {
    return initialTheme ?? themeManager.getTheme(); // 取得 Default Theme
  });

  useEffect(() => {
    themeManager.setTheme(theme);
  }, [theme]);

  const toggleTheme = useCallback((theme) => {
    setTheme((prev) => (prev === THEME.DARK ? THEME.LIGHT : THEME.DARK));
  }, []);

  return useMemo(
    () => ({
      theme,
      toggleTheme,
      setTheme,
    }),
    [theme, toggleTheme],
  );
}

export default () => {
  const { theme, toggleTheme } = useTheme();

  return (
    <div className="container">
      <link
        rel="stylesheet"
        href="https://cdn.jsdelivr.net/npm/@tocino-ui/design-tokens/dist/normalize/normalize.css"
      />
      <h2 className="header">Current Theme: {theme}</h2>
      <Button variant="outline" onClick={toggleTheme}>
        Toggle Theme
      </Button>
    </div>
  );
};

可以看到上面引入了我們先前做的 Button 組件,並且透過 useTheme 去控制 data-theme 的值,這樣就可以讓設計系統能夠支援切換深淺色主題。