在 tailwind css 中实现一键切换颜色主题

4,803 阅读2分钟

在 tailwind css 中实现一键切换颜色主题

主题切换是非常常见的需求,但是 tailwind css官方文档中只有固定主题配置的介绍,无法做到动态的切换主题。今天向大家介绍在 react 项目中,使用css变量实现动态切换颜色主题的方法。

themeSwitch.gif

一、tailwind css 相关配置

1.在全局 css 中添加颜色主题

/* index.css */
@tailwind base;
@tailwind components;
@tailwind utilities;

.theme-nord {
  --color-primary: 94 129 172;
  --color-secondary: 129 161 193;
  --color-background: 46 52 64;
  --color-foreground: 216 222 233;
  --color-separator: 76 86 106;
}

.theme-solarizedDark {
  --color-primary: 38 139 210;
  --color-secondary: 203 75 22;
  --color-background: 0 43 54;
  --color-foreground: 238 232 213;
  --color-separator: 131 148 150;
}

.theme-solarizedLight {
  --color-primary: 211 54 130;
  --color-secondary: 133 153 0;
  --color-background: 253 246 227;
  --color-foreground: 101 123 131;
  --color-separator: 88 110 117;
}

我们的主题类名有个统一的theme前缀,这是为了防止与自定义的 tailwind 类型冲突,你可以随意修改它。另外,注意到我们的颜色色值都是使用R G B这样的格式,这是为了匹配 tailwind 对颜色透明度配置的写法,如bg-primary[0.2]如果采用 16 进制写法的话,这将是个无效类名。如有大佬有办法解决的话,欢迎评论区指出~

2.配置 tailwind.config.js

// tailwind.config.js
/** @type {import('tailwindcss').Config} */

export default {
  content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
  theme: {
    extend: {
      backgroundColor: {
        primary: "rgb(var(--color-primary) / <alpha-value>)",
        secondary: "rgb(var(--color-secondary) / <alpha-value>)",
        background: "rgb(var(--color-background) / <alpha-value>)",
        foreground: "rgb(var(--color-foreground) / <alpha-value>)",
        separator: "rgb(var(--color-separator) / <alpha-value>)",
      },
      textColor: {
        primary: "rgb(var(--color-foreground) / <alpha-value>)",
      },
    },
  },
  plugins: [],
  darkMode: "class",
};

重点关注backgroundColortextColor这两个对象中的内容,他们分别是背景颜色和文字颜色的配置。其中的各种属性名就是我们在元素类名中使用的颜色名称,即bg-primary将会呈现--color-primary这个 css 变量的颜色。那么,"rgb(var(--color-primary) / <alpha-value>)"这一坨是什么意思呢?这就是实现透明度配置bg-primary[0.2]的写法,参见官方文档。如此,tailwind 方面的配置就做完了,接下来我们在 react 中实现一个主题切换小 demo。

二、React 主题切换 demo

1.实现主题切换基础版

// App.tsx
import React, { useState } from "react";

function App() {
  const themes = ["nord", "solarizedDark", "solarizedLight"];
  const [theme, setTheme] = useState("nord");

  return (
    <div
      className={`
        theme-${theme}
        bg-background
        w-screen h-screen 
        flex flex-col justify-center items-center
      `}
    >
      <p className="mb-10 text-primary">当前主题:{theme}</p>
      <span className="text-primary/[0.5]">点击下方按钮切换主题</span>
      <div className="mt-10">
        {themes.map((theme) => (
          <button
            className="border rounded p-2 mr-5 bg-secondary text-white"
            onClick={() => setTheme(theme)}
          >
            {theme}
          </button>
        ))}
      </div>
    </div>
  );
}

export default App;

还记得我们主题的命名规则吗?theme+主题名,我们将这个类名加到最顶层元素中,通过通过状态切换来改变主题类名,由此就完成了颜色变量的动态切换,配合tailwind.config.js中对backgroundColortextColor的配置,由此bg-primarytext-primary这两个类名让我们实现了颜色的动态切换。

themeSwitch.gif

2.实现主题切换进阶版

基础版 demo 实现了需求,但是有亿点小问题。首先是没有持久化,每次刷新页面就变回了默认的主题,其次是没有redux这样的全局状态管理,我们在子组件中实现切换主题会很麻烦。以上两个问题都可以通过引入状态管理库解决,目前有很多选择比如reduxmobxzustand等。我这边决定使用zustand

实现useThemeStore

// store/themeStore.ts
import { create } from "zustand";
import { persist, createJSONStorage } from "zustand/middleware";

type theme = "solarizedDark" | "nord" | "solarizedLight";

type themeStore = {
  themes: theme[];
  activeTheme: theme;
  setTheme: (theme: theme) => void;
};

const useThemeStore = create<themeStore>()(
  // 持久化中间件
  persist(
    (set) => ({
      themes: ["solarizedDark", "nord", "solarizedLight"],
      activeTheme: "nord",
      setTheme: (activeTheme: theme) => set(() => ({ activeTheme })),
    }),
    {
      name: "active-theme", // 存储在storage中的key名
      // storage: createJSONStorage(() => sessionStorage), // 存储数据库配置,默认使用localstorage
      // 过滤函数
      partialize: (state) =>
        Object.fromEntries(
          Object.entries(state).filter(([key]) => key === "activeTheme")
        ),
    }
  )
);

export default useThemeStore;

跟着zustand 文档就能简单实现。注意persist这个函数是实现持久化用的,第二个入参对象中的name属性就是我们存储的 key 值,storage属性允许我们自定义存储的数据库,默认使用localstorage

在组件中使用

// App.tsx
import React from "react";
import useThemeStore from "./stores/themeStore";

function App() {
  const { themes, activeTheme, setTheme } = useThemeStore((state) => state);

  return (
    <div
      className={`
        theme-${activeTheme}
        bg-background
        w-screen h-screen 
        flex flex-col justify-center items-center
      `}
    >
      <p className="mb-10 text-primary">当前主题:{activeTheme}</p>
      <span className="text-primary/[0.5]">点击下方按钮切换主题</span>
      <div className="mt-10">
        {themes.map((theme) => (
          <button
            className="border rounded p-2 mr-5 bg-secondary text-white"
            onClick={() => setTheme(theme)}
          >
            {theme}
          </button>
        ))}
      </div>
    </div>
  );
}

export default App;

现在查看localstorage就可以看到我们存储的对象,这样即便刷新页面我们选择的主题也会保存。同时useThemeStore这个 hook 可以在任意组件中使用,于是子组件切换主题也得以实现了。

local.png