antd + tailwindcss 如何实现统一自由的主题配置?

2,571 阅读6分钟

#创作者训练营

本教程所使用 antd 版本为截止发稿前的最新版本 5.25.1;同时默认读者了解并使用 antd 做组件开发。

1. 前言

antd 是当前最火的前端页面开发组件库之一,我们结合 antd 的 Design Token 可以实现一套方便自由统一的主题切换系统,但同时页面总有一些样式或组件细节是 antd 不能全部覆盖的,那就需要我们使用 less/tailwindcss/sass/css-in-js 等方式或者纯 css 自己写样式,如果同时我们想使用/继承 antd 主题中定义的一些样式,如主题色、文字色、border radius尺寸等,要怎么做呢?本篇主要讲通过 antd 暴露的 css 变量来实现主题样式的全局使用方法。

编者注:antd 官方文档已经指明了如何实现主题配置和切换,不过是分散在几篇文档中,本篇主要做汇总并且加一些自我理解和实践。

2. 快速开始

官方文档指路:定制主题 - Ant Design

2.1. 我们可以建一个全局的 theme.ts 文件来定义一些我们的主题样式(编者目前是只要 light 模式和 dark 模式)

// src/theme.ts
export const lightColors = {
  'primary': '#2979FF',        // 主色 - 高亮科技蓝
  'accent': '#5C9CFF',         // 辅助色 - 稍浅蓝,用于 hover 等
  'selectedBg': '#D4EBFF',     // 选中背景色 - 浅蓝
  'background': '#F9FAFB',     // 背景色 - 浅灰白
   ...
};

export const darkColors = {
  'primary': '#5C9CFF',        // 主色(提亮一点)
  'accent': '#8CBBFF',         // 辅助色(更亮)
  'selectedBg': '#1d3466',     // 选中背景色 - 浅蓝
  'background': '#0F172A',     // 背景色 - 深蓝黑
   ...
}
// 风格(比如 box-shadow 等)
export const lightStyle = {
  'primaryShadow': '0 2px 0 rgb(42 121 255 / 10%)',
  ... 
}
export const darkStyle = {
  'primaryShadow': '0 2px 0 rgb(92 156 255 / 10%)',
  ...
}

2.2. 创建一个 antd 的主题配置文件

// antdThemeConfig.ts
import { lightColors, darkColors, lightStyle, darkStyle } from '../theme';
import { theme, ThemeConfig } from 'antd';
// 根据 isDark 判断获取哪种主题样式
export const getThemeConfig = (isDark: boolean): ThemeConfig => {
  const colors = isDark ? darkColors : lightColors;
  const styles = isDark ? darkStyle : lightStyle;
  return {
    token: { // 全局 Design Token 配置
      colorPrimary: colors['primary'],
      colorBgBase: colors['background'],
      colorTextBase: colors['text'],
      colorInfo: colors['primary'],
      colorSuccess: colors['success'],
      colorWarning: colors['warning'],
      colorError: colors['danger'],
      colorBorder: colors['divider'],
    },
    cssVar: {
      key: 'r-theme',
    },
    hashed: false, // 根据官方文档,如果应用中只存在一个版本的 antd,可以选择关闭 hash 来进一步减小样式体积
    algorithm: theme.defaultAlgorithm,
    components: { // 组件 token 配置
      Button: {
        colorPrimaryHover: colors['accent'],
        primaryShadow: styles['primaryShadow'],
        dangerShadow: styles['dangerShadow'],
      },
      Menu: {
        itemActiveBg: styles['itemActiveBg'],
        itemSelectedBg: colors['selectedBg'],
      },
      Layout: {
        headerBg: colors['card'],
      },
    },
  };
}

文档中的配置属性说明如下:

  • token

按照官方文档,token 分为基础变量(Seed Token)、梯度变量(Map Token)、别名变量(Alias Token),简单来说的话,Seed Token 是后面衍生出梯度变量和别名变量的基础,因为 antd 内部有一套计算这些变量的算法(下面就有)。我的建议是我们在这个属性下面主要可以设定一些基础主题变量(Seed Token),比如 colorPrimary 等,Seed Token 的全量列表可以看这里 - ant.design/docs/react/…

  • algorithm

默认是 defaultAlgorithm,还有 compactAlgorithm, darkAlgorithm,可以组合使用。从目前看下来,有一些梯度颜色生成的不是特别好,所以需要我们根据情况自己再去设定。

  • cssVar

从名字就可以看出来,这个属性表示 antd 内部的 css 变量,设置这个可以将他的所有 css 变量暴露出来供我们自己设置样式时使用,我强烈推荐配置这个变量。 至于里面的 key 等配置大家可以看官方文档自行了解,暂不赘述

  • hashed

官方文档建议如果项目只有一个 antd 版本,可以选择关闭 hashed,这样可以进一步减少体积。

  • components

官方说明:“用于修改 Component Token 以及覆盖该组件消费的 Alias Token”。除了通用的 Design Token,antd 的每个组件有自己特定的一些 token,如果要对特定组件的样式做修改,可以放在 components 下面进行配置,这样可以统一所有该组件在项目中的样式,比如说希望全局的 modal 样式是统一的,那么我们可以直接在这里去配置样式,而不是不同的开发者按照设计稿又自己修改样式,导致代码冗余。

2.3. 主题切换

接下来我们来看一下如何使用这些配置

  • 创建 ThemeContext
// context/ThemeContext.tsx

import React, { createContext, useContext, useState, useEffect } from "react";

export type ThemeContextType = {
  isDark: boolean;
  toggleTheme: () => void;
};

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

export const ThemeProvider = ({ children }: { children: React.ReactNode }) => {
  const [isDark, setIsDark] = useState(false);

  useEffect(() => {
    // 初始化时读取 localStorage 或系统偏好
    const savedTheme = localStorage.getItem("theme") === "dark";
    setIsDark(savedTheme);
  }, []);

  const toggleTheme = () => {
    const newTheme = !isDark;
    setIsDark(newTheme);
    localStorage.setItem("theme", newTheme ? "dark" : "light");
    document.documentElement.classList.toggle("dark", newTheme);
  };

  return (
    <ThemeContext.Provider value={{ isDark, toggleTheme }}>
      {children}
    </ThemeContext.Provider>
  );
};

export const useTheme = () => {
  const context = useContext(ThemeContext);
  if (!context) throw new Error("useTheme must be used within ThemeProvider");
  return context;
};
  • 入口文件添加以下代码
// 入口文件 App.tsx 或者根 index.tsx
import { RouterProvider } from "react-router";
import { Provider } from "react-redux";
import { ThemeProvider, useTheme } from "@/context/ThemeContext";

const AppContent: React.FC = () => {
  const { isDark } = useTheme();
  
  useEffect(() => {
    if (isDark) {
      document.documentElement.classList.add('dark');
    } else {
      document.documentElement.classList.remove('dark');
    }
  }, [isDark]);

  return (
    <ThemeProvider>
      <Provider store={store}>
        <ConfigProvider theme={getThemeConfig(isDark)}>
          <RouterProvider router={router} />
        </ConfigProvider>
      </Provider>
    </ThemeProvider>
    
  );
}

注:以上第 7-13 行至关重要,这块代码是实现 tailwindcss 可以切换暗黑模式的重点

  • 添加切换主题按钮
// ../layout.tsx

import { useTheme } from "@/context/ThemeContext";
import { SunOutlined, MoonOutlined } from "@ant-design/icons";
const MainLayout = () => {
  const { isDark, toggleTheme } = useTheme();
  {...} // 省略
  return (
    <>
    {...}
      <Segmented
        size="small"
        shape="round"
        options={[
          { value: 'light', icon: <SunOutlined /> },
          { value: 'dark', icon: <MoonOutlined /> },
        ]}
        onChange={toggleTheme}
        value={isDark ? 'dark' : 'light'}
      />
    </>
  )

示例

2.4. tailwind css (v4+)使用

  • 首先需要配置 tailwind.config.js
/** @type {import('tailwindcss').Config} */

export default {
  content: ["./src/**/*.{js,jsx,ts,tsx}"],
  darkMode: 'class',
  plugins: [],
};

注意这儿的darkMode要设置为class.

  • 使用代码
// tailwind css 使用,可以直接使用 antd 暴露出来的变量
<Button onClick={handleReset} className='bg-[var(--ant-color-error)]! dark:bg-[var(--ant-color-warning)]!'>重置</Button>

// dark 模式的样式配置
<Button onClick={handleReset} className='bg-[var(--ant-color-error)]! dark:bg-[var(--ant-color-warning)]!'>重置</Button>

2.5. less 文件使用

可直接使用 antd 的 css 变量

/* less 文件 */
.ant-table-thead > tr > th {
  text-align: center !important;
  font-weight: 500;
  background-color: var(--ant-color-primary);
}

2.6. 我们如何知道要修改哪个 Design Token 值呢(重要❗️)

接下来就是一些细节问题了,我们目前配置了大方向的主题,包括主色,背景色,文字色,但是可能有一些组件属性,比如 box-shadow,他跟我们的主色并不相配,如下图所示,我们发现在暗黑模式下的按钮显示有问题

经过排查,发现是以下这个属性的原因,他所对应的变量是 --ant-button-primary-shadow

那么,这个 css 变量对应的 Design Token 是什么呢?当然一种办法是我们可以在 global.css 中修改

--ant-button-primary-shadow 变量的值来实现我们的目的,但是我们需要有两个值,他们分别是在 light 模式和 dark 模式下,那在 css 中怎么判断当前是出于 light 还是 dark 模式呢,这个我暂时还没有好的办法;所以还有一种办法就是找到他的 Design Token,我们直接在前文提到的 antdThemeConfig.ts 中修改,那么怎么寻找呢?

  • 先看这个变量是哪个组件的

我们这个是 Button 组件

  • 然后去对应组件的官方文档下去查找他对应的 Design Token

我们去查阅 ant.design/components/… 官方文档,果然找到了他的 Design Token

  1. 接下来我们可以修改我们的配置文件了
// theme.ts
export const lightStyle = {
  'primaryShadow': '0 2px 0 rgb(42 121 255 / 10%)',
}
export const darkStyle = {
  'primaryShadow': '0 2px 0 rgb(92 156 255 / 10%)',
}

// antdThemeConfig.ts
export const getThemeConfig = (isDark: boolean): ThemeConfig => {
  ...
  return {
    token: {
      ...
    },
    cssVar: {
       ...
    },
    hashed: false, // 根据官方文档,如果应用中只存在一个版本的 antd,你可以选择关闭 hash 来进一步减小样式体积
    algorithm: theme.defaultAlgorithm,
    components: {
      Button: {
        primaryShadow: styles['primaryShadow'],
      },
      ...
    },
  };
}

大功告成!

3. Bonus时间: 插件

  1. cursor 可以安装 Tailwind CSS IntelliSense,写 tailwind 语法会有提示

  1. 可以安装 Color Highlight 插件,可以高亮颜色