跳到主要内容
预计阅读 53 分钟

样式方案与主题系统 —— 让界面美观又可维护

前面几章我们已经学会了用组件搭建 UI、用 state 管理数据、用 Hooks 处理副作用。但是一个应用光有骨架远远不够——用户看到的第一眼是样式,而开发者噩梦的第一名也是样式。全局污染、命名冲突、主题切换困难……这些问题在 React 中如何优雅地解决?这一章,我们就来系统梳理 React 生态中的主流样式方案,并亲手实现一套完整的明暗主题系统。

📋 开篇自测:你已经知道多少?

  1. 你能说出普通 CSS 在 React 项目中至少两个致命缺陷吗?
  2. CSS Modules 和 CSS-in-JS 在”样式隔离”这件事上,分别是在哪个阶段完成的——编译时还是运行时?
  3. 如果让你实现一个明暗主题切换功能,你会选择什么技术方案?

一、React 中的样式挑战 —— 为什么普通 CSS 不够用

在传统的多页面应用中,每个页面的 CSS 文件相对独立,样式冲突的概率不高。但 React 是单页应用(SPA),所有组件最终都渲染到一个页面里,所有 CSS 也都汇聚到同一个全局作用域中。这就像把十几个人的衣柜里的衣服全倒在一间房里——名字撞了,东西就乱了。

1.1 全局污染与命名冲突

假设你有两个组件,各自定义了 .btn 样式:

/* Button1.css */
.btn {
  background: blue;
  padding: 12px 24px;
}

/* Button2.css */
.btn {
  background: green;
  padding: 8px 16px;
}
// Button1.jsx
import './Button1.css';
function Button1() {
  return <button className="btn">主要按钮</button>;
}

// Button2.jsx
import './Button2.css';
function Button2() {
  return <button className="btn">次要按钮</button>;
}

在同一个页面渲染时,后加载的样式会覆盖先加载的,两个按钮看起来一模一样。你以为 import './Button1.css' 只对 Button1 生效?不,CSS 没有模块作用域,import 只是告诉打包工具”把这个 CSS 文件打进来”,样式照样是全局的。

1.2 样式与组件的耦合困境

除了命名冲突,普通 CSS 还有几个痛点:

  • 删除组件时不敢删样式:不知道这段 CSS 有没有被其他组件用到
  • 样式复用靠人工:同样的 padding: 16px; border-radius: 8px; 在十个 class 里写了十遍
  • 动态样式要写大量条件 classclassName={isActive ? 'btn btn-active' : 'btn'} 这种拼接越来越长

这些问题催生了 React 社区的各种样式方案。接下来我们用同一个卡片组件,依次体验每种方案的写法和效果。


二、内联样式 —— 简单直接但有局限

React 允许你用 JavaScript 对象直接写样式,通过 style 属性传入:

function ProfileCard({ name, avatar }) {
  const cardStyle = {
    border: '1px solid #e0e0e0',
    borderRadius: '12px',
    padding: '24px',
    textAlign: 'center',
    maxWidth: '300px',
    boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
  };

  const nameStyle = {
    fontSize: '18px',
    fontWeight: 600,
    color: '#333',
    marginTop: '12px',
  };

  return (
    <div style={cardStyle}>
      <img
        src={avatar}
        alt={name}
        style={{ width: '80px', height: '80px', borderRadius: '50%' }}
      />
      <div style={nameStyle}>{name}</div>
    </div>
  );
}

内联样式的优势很明显:

  • 天然没有命名冲突——样式直接绑定在元素上
  • 可以直接使用 JavaScript 变量和表达式,动态样式信手拈来
  • 不需要任何额外的工具或配置

局限性同样突出:

  • 不支持伪类和伪元素hoverfocus::before 这些统统不行
  • 不支持媒体查询:响应式布局无法实现
  • 不支持动画的 @keyframes
  • 性能隐患:每次渲染都会创建新的样式对象,频繁触发内联样式的序列化

所以内联样式只适合非常简单的、不需要伪类和响应式的场景,比如动态设置一个元素的 widthcolor。对于完整的组件样式,我们需要更强大的方案。

🤔 想一想 如果你在内联样式中写了 style={{ background: 'red' }},而外部 CSS 里也有 .card { background: blue; } 并且给了同一个元素 className="card",最终背景色是什么?为什么?


三、CSS Modules —— 自动作用域隔离

CSS Modules 是目前 React 项目中最主流的样式隔离方案之一。它的核心思路极其简单:编译时自动给每个 className 加上唯一的 hash 后缀,从根本上杜绝了命名冲突。

3.1 基本用法

在 Vite 或 CRA 项目中,只要把 CSS 文件命名为 *.module.css,就自动开启 CSS Modules:

/* ProfileCard.module.css */
.card {
  border: 1px solid #e0e0e0;
  border-radius: 12px;
  padding: 24px;
  text-align: center;
  max-width: 300px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}

.avatar {
  width: 80px;
  height: 80px;
  border-radius: 50%;
}

.name {
  font-size: 18px;
  font-weight: 600;
  color: #333;
  margin-top: 12px;
}

.card:hover {
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
  transform: translateY(-2px);
  transition: all 0.2s ease;
}
import styles from './ProfileCard.module.css';

function ProfileCard({ name, avatar }) {
  return (
    <div className={styles.card}>
      <img src={avatar} alt={name} className={styles.avatar} />
      <div className={styles.name}>{name}</div>
    </div>
  );
}

编译后,.card 会变成类似 _card_x3k2j_1 这样的唯一 className。你在开发时只关心语义化的 .card,编译器帮你处理隔离问题。

3.2 组合多个 class

当需要动态添加 class 时,可以用模板字符串或 classnames 库:

import styles from './ProfileCard.module.css';
import classNames from 'classnames';

function ProfileCard({ name, avatar, isVip }) {
  return (
    <div className={classNames(styles.card, { [styles.vip]: isVip })}>
      <img src={avatar} alt={name} className={styles.avatar} />
      <div className={styles.name}>{name}</div>
    </div>
  );
}

3.3 全局与局部混用

如果某个样式确实需要全局生效(比如第三方库的覆盖样式),用 :global() 包裹:

/* ProfileCard.module.css */
.card :global(.ant-btn) {
  border-radius: 8px;
}

这样 .card 会被加 hash,但 .ant-btn 保持原样,精确地覆盖了 Ant Design 按钮的圆角。

CSS Modules 的优缺点小结:

  • 编译时隔离,零运行时开销
  • 写法与普通 CSS 几乎一致,学习成本极低
  • 可以与 SCSS/LESS 搭配使用(命名为 *.module.scss
  • 动态样式能力较弱,依赖 className 的条件拼接
  • 样式和组件分散在两个文件中,需要来回跳转

四、CSS-in-JS —— styled-components 与 Emotion

CSS-in-JS 的理念是用 JavaScript 来写 CSS,把样式和组件逻辑放在同一个文件里。styled-components 和 Emotion 是这个领域最流行的两个库。

4.1 styled-components 基本用法

安装:

npm install styled-components

用 styled-components 重写同一个卡片组件:

import styled from 'styled-components';

const Card = styled.div`
  border: 1px solid #e0e0e0;
  border-radius: 12px;
  padding: 24px;
  text-align: center;
  max-width: 300px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);

  &:hover {
    box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
    transform: translateY(-2px);
    transition: all 0.2s ease;
  }
`;

const Avatar = styled.img`
  width: 80px;
  height: 80px;
  border-radius: 50%;
`;

const Name = styled.div`
  font-size: 18px;
  font-weight: 600;
  color: ${props => props.$isVip ? '#e6a23c' : '#333'};
  margin-top: 12px;
`;

function ProfileCard({ name, avatar, isVip }) {
  return (
    <Card>
      <Avatar src={avatar} alt={name} />
      <Name $isVip={isVip}>{name}</Name>
    </Card>
  );
}

注意 Name 组件中直接通过 props 控制颜色——这是 CSS-in-JS 最大的卖点之一:样式可以像普通 React 组件一样接收 props,用 JavaScript 逻辑来动态决定样式

4.2 样式继承与扩展

styled-components 支持基于已有样式组件创建变体:

const PrimaryButton = styled.button`
  padding: 10px 24px;
  font-size: 14px;
  border: none;
  border-radius: 6px;
  background: #1677ff;
  color: white;
  cursor: pointer;

  &:hover {
    background: #4096ff;
  }
`;

const DangerButton = styled(PrimaryButton)`
  background: #ff4d4f;

  &:hover {
    background: #ff7875;
  }
`;

DangerButton 继承了 PrimaryButton 的全部样式,只覆盖了背景色。

4.3 Emotion:另一个选择

Emotion 和 styled-components 的 API 非常相似,但它额外提供了 css prop 的写法,可以更灵活:

npm install @emotion/react @emotion/styled
/** @jsxImportSource @emotion/react */
import { css } from '@emotion/react';
import styled from '@emotion/styled';

// 方式一:styled API(和 styled-components 几乎一样)
const Card = styled.div`
  border-radius: 12px;
  padding: 24px;
`;

// 方式二:css prop(不需要创建额外的样式组件)
function ProfileCard({ name }) {
  return (
    <div
      css={css`
        border-radius: 12px;
        padding: 24px;
        &:hover {
          box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
        }
      `}
    >
      <span>{name}</span>
    </div>
  );
}

CSS-in-JS 的优缺点小结:

  • 样式与组件共存一个文件,高内聚
  • 动态样式能力极强,直接用 JS 逻辑控制
  • 自动生成唯一 className,零冲突
  • 有运行时开销——样式在浏览器端动态生成和注入
  • 组件树中会出现大量样式组件包裹层,DevTools 调试稍显繁琐
  • bundle 体积会增加(styled-components 约 13KB gzipped)

🤔 想一想 如果一个页面有 200 个组件,每个组件用 styled-components 创建了 3-5 个样式组件,那运行时一共需要注入多少条 CSS 规则?这对首屏性能会有什么影响?你能想到什么优化手段吗?


五、Tailwind CSS —— 原子化样式的新范式

Tailwind CSS 代表了一种完全不同的思路:不写任何自定义 CSS,而是用预定义的原子 class 来组合出任意样式

5.1 安装与配置

在 Vite + React 项目中:

npm install -D tailwindcss @tailwindcss/vite

vite.config.ts 中注册插件:

import tailwindcss from '@tailwindcss/vite';

export default defineConfig({
  plugins: [react(), tailwindcss()],
});

在入口 CSS 文件中引入 Tailwind:

/* index.css */
@import 'tailwindcss';

5.2 用 Tailwind 重写卡片组件

function ProfileCard({ name, avatar, isVip }) {
  return (
    <div className="border border-gray-200 rounded-xl p-6 text-center max-w-xs shadow-md hover:shadow-lg hover:-translate-y-0.5 transition-all duration-200">
      <img
        src={avatar}
        alt={name}
        className="w-20 h-20 rounded-full mx-auto"
      />
      <div className={`text-lg font-semibold mt-3 ${isVip ? 'text-amber-500' : 'text-gray-800'}`}>
        {name}
      </div>
    </div>
  );
}

整个组件没有写一行 CSS,所有样式都是 Tailwind 提供的原子 class。rounded-xl 就是 border-radius: 0.75remp-6 就是 padding: 1.5rem——你不需要记住这些,安装 Tailwind CSS IntelliSense 插件后会有完整的智能提示。

5.3 Tailwind 为什么能火

三个核心价值打动了大量开发者:

不用起 class 名字:这是很多人最喜欢的一点。命名是计算机科学中公认的两大难题之一,Tailwind 让你彻底告别 .card-wrapper-inner-content-title 这种绝望。

CSS 不会无限增长:传统写法中,每新增一个组件就要新增一批 CSS。而 Tailwind 的原子 class 是复用的——整个项目可能只用到几百个不同的 class,最终生成的 CSS 文件很小。

样式不会互相干扰:每个样式只作用于当前标签,不存在级联覆盖的问题。

5.4 处理重复的 class 组合

当相同的 class 组合在多处出现时,有两种解法:

方式一:抽组件(推荐)

function Badge({ children, variant = 'default' }) {
  const base = 'px-3 py-1 rounded-full text-sm font-medium';
  const variants = {
    default: 'bg-gray-100 text-gray-800',
    success: 'bg-green-100 text-green-800',
    danger: 'bg-red-100 text-red-800',
  };

  return <span className={`${base} ${variants[variant]}`}>{children}</span>;
}

方式二:@apply 指令

/* 在 CSS 文件中 */
.btn-primary {
  @apply px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors;
}

不过 Tailwind 官方更推荐方式一——既然用了 React,组件本身就是最好的抽象单元。


六、主题系统实现 —— CSS 变量 + Context 实现明暗主题切换

掌握了各种样式方案后,我们来解决一个实际需求:让应用支持明暗主题切换。这个功能的核心原理只有一句话——用 CSS 变量定义所有颜色,切换主题时切换变量的值

6.1 定义主题变量

/* theme.css */
:root {
  /* 亮色主题(默认) */
  --color-bg: #ffffff;
  --color-bg-secondary: #f5f5f5;
  --color-text: #333333;
  --color-text-secondary: #666666;
  --color-border: #e0e0e0;
  --color-primary: #1677ff;
  --color-primary-hover: #4096ff;
  --color-shadow: rgba(0, 0, 0, 0.1);
}

[data-theme='dark'] {
  --color-bg: #1a1a2e;
  --color-bg-secondary: #16213e;
  --color-text: #e0e0e0;
  --color-text-secondary: #a0a0a0;
  --color-border: #2a2a4a;
  --color-primary: #4096ff;
  --color-primary-hover: #69b1ff;
  --color-shadow: rgba(0, 0, 0, 0.3);
}

亮色主题写在 :root 上作为默认值,暗色主题写在 [data-theme='dark'] 选择器上。当根元素的 data-theme 属性变为 dark 时,所有变量值自动切换——因为 CSS 变量会在元素和其所有子元素中生效。

6.2 用 Context 管理主题状态

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

const ThemeContext = createContext();

export function ThemeProvider({ children }) {
  // 从 localStorage 读取用户之前的选择,没有就默认 light
  const [theme, setTheme] = useState(() => {
    return localStorage.getItem('theme') || 'light';
  });

  useEffect(() => {
    // 把主题值同步到 DOM 的 data-theme 属性上
    document.documentElement.setAttribute('data-theme', theme);
    // 持久化到 localStorage
    localStorage.setItem('theme', theme);
  }, [theme]);

  const toggleTheme = () => {
    setTheme(prev => (prev === 'light' ? 'dark' : 'light'));
  };

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

export function useTheme() {
  const context = useContext(ThemeContext);
  if (!context) {
    throw new Error('useTheme 必须在 ThemeProvider 内部使用');
  }
  return context;
}

6.3 在组件中使用主题变量

主题变量生效后,组件里只需要用 var(--xxx) 引用即可——不管你用的是 CSS Modules、styled-components 还是 Tailwind:

CSS Modules 写法:

/* ProfileCard.module.css */
.card {
  background: var(--color-bg);
  border: 1px solid var(--color-border);
  color: var(--color-text);
  box-shadow: 0 2px 8px var(--color-shadow);
}

styled-components 写法:

const Card = styled.div`
  background: var(--color-bg);
  border: 1px solid var(--color-border);
  color: var(--color-text);
  box-shadow: 0 2px 8px var(--color-shadow);
`;

Tailwind 写法:

Tailwind 4.0 支持直接在 CSS 中定义自定义主题 token。如果你使用的是较早版本,可以在 tailwind.config.js 中扩展颜色:

// tailwind.config.js
module.exports = {
  theme: {
    extend: {
      colors: {
        bg: 'var(--color-bg)',
        'bg-secondary': 'var(--color-bg-secondary)',
        'text-primary': 'var(--color-text)',
        'text-secondary': 'var(--color-text-secondary)',
      },
    },
  },
};

然后就可以在 JSX 中这样写:

<div className="bg-bg text-text-primary border border-[var(--color-border)]">
  主题感知的卡片
</div>

6.4 主题切换按钮

import { useTheme } from './ThemeContext';

function ThemeToggle() {
  const { theme, toggleTheme } = useTheme();

  return (
    <button
      onClick={toggleTheme}
      style={{
        background: 'var(--color-bg-secondary)',
        color: 'var(--color-text)',
        border: '1px solid var(--color-border)',
        borderRadius: '8px',
        padding: '8px 16px',
        cursor: 'pointer',
        fontSize: '16px',
      }}
    >
      {theme === 'light' ? '🌙 暗色模式' : '☀️ 亮色模式'}
    </button>
  );
}

6.5 跟随系统主题

很多用户的操作系统本身就设置了暗色模式。我们可以在没有用户手动选择时,自动跟随系统设置:

const [theme, setTheme] = useState(() => {
  const saved = localStorage.getItem('theme');
  if (saved) return saved;
  // 没有手动选择过,跟随系统
  return window.matchMedia('(prefers-color-scheme: dark)').matches
    ? 'dark'
    : 'light';
});

🤔 想一想 为什么我们选择 CSS 变量 + data-theme 属性来实现主题切换,而不是在 React 的 Context 中存储所有颜色值、在每个组件中通过 useTheme() 读取颜色?这两种方案在性能上有什么本质区别?


七、方案对比与选型指南

讲了这么多方案,你一定想问:到底该用哪个?下面这张表帮你一目了然地做出选择:

对比维度内联样式CSS Modulesstyled-componentsEmotionTailwind CSS
样式隔离天然隔离编译时哈希运行时生成唯一 class运行时生成唯一 class原子 class 无冲突
动态样式原生支持需拼接 classNameprops 驱动,极强props 驱动,极强条件拼接 class
伪类/媒体查询不支持完整支持完整支持完整支持完整支持
运行时开销极低零(纯 CSS)有(~13KB gzip)有(~7KB gzip)零(纯 CSS)
开发体验差(对象写法)好(写标准 CSS)好(模板字符串)好(双模式)优(即写即见)
学习成本极低中(记 class 名)
TypeScript 支持天然需额外插件良好良好天然
SSR 支持需额外配置需额外配置
社区与生态-构建工具原生支持非常成熟非常成熟极其火爆
适用场景简单动态值中大型项目组件库/需要高动态组件库/需要高动态所有项目

选型建议:

  • 个人项目、快速原型:直接上 Tailwind CSS,开发速度最快
  • 团队协作的中大型项目:CSS Modules + SCSS 是最稳妥的选择,学习成本最低
  • 组件库开发:CSS-in-JS(styled-components 或 Emotion)在动态主题和样式定制方面优势明显
  • 追求极致性能:CSS Modules 或 Tailwind,两者都是零运行时方案
  • 混合使用:完全可行。比如用 Tailwind 做布局和常规样式,用 CSS Modules 做复杂组件的内部样式

八、实战:构建一个支持主题切换的组件库基础

现在让我们把前面学到的知识串起来,构建一个完整的、支持明暗主题切换的迷你组件库。

8.1 项目结构

src/
├── theme/
│   ├── ThemeContext.jsx    # 主题 Context
│   └── theme.css           # 主题变量定义
├── components/
│   ├── Button/
│   │   ├── Button.jsx
│   │   └── Button.module.css
│   ├── Card/
│   │   ├── Card.jsx
│   │   └── Card.module.css
│   └── ThemeToggle/
│       └── ThemeToggle.jsx
└── App.jsx

8.2 主题变量(theme.css)

:root {
  --color-bg: #ffffff;
  --color-bg-elevated: #f8f9fa;
  --color-text: #1a1a2e;
  --color-text-muted: #6c757d;
  --color-border: #dee2e6;
  --color-primary: #1677ff;
  --color-primary-hover: #4096ff;
  --color-primary-text: #ffffff;
  --color-danger: #ff4d4f;
  --color-danger-hover: #ff7875;
  --color-shadow: rgba(0, 0, 0, 0.08);
  --radius: 8px;
  --transition: all 0.2s ease;
}

[data-theme='dark'] {
  --color-bg: #0f0f23;
  --color-bg-elevated: #1a1a35;
  --color-text: #e8e8e8;
  --color-text-muted: #8b8b9e;
  --color-border: #2d2d50;
  --color-primary: #4096ff;
  --color-primary-hover: #69b1ff;
  --color-primary-text: #ffffff;
  --color-danger: #ff6b6b;
  --color-danger-hover: #ff8e8e;
  --color-shadow: rgba(0, 0, 0, 0.25);
  --radius: 8px;
  --transition: all 0.2s ease;
}

8.3 ThemeContext

// theme/ThemeContext.jsx
import { createContext, useContext, useState, useEffect } from 'react';

const ThemeContext = createContext(null);

export function ThemeProvider({ children }) {
  const [theme, setTheme] = useState(() => {
    const saved = localStorage.getItem('app-theme');
    if (saved) return saved;
    return window.matchMedia('(prefers-color-scheme: dark)').matches
      ? 'dark'
      : 'light';
  });

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

  const toggleTheme = () =>
    setTheme(prev => (prev === 'light' ? 'dark' : 'light'));

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

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

8.4 Button 组件

/* components/Button/Button.module.css */
.button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 8px 20px;
  font-size: 14px;
  font-weight: 500;
  border: 1px solid transparent;
  border-radius: var(--radius);
  cursor: pointer;
  transition: var(--transition);
  line-height: 1.5;
}

.primary {
  background: var(--color-primary);
  color: var(--color-primary-text);
}

.primary:hover {
  background: var(--color-primary-hover);
}

.danger {
  background: var(--color-danger);
  color: var(--color-primary-text);
}

.danger:hover {
  background: var(--color-danger-hover);
}

.outlined {
  background: transparent;
  color: var(--color-primary);
  border-color: var(--color-primary);
}

.outlined:hover {
  color: var(--color-primary-hover);
  border-color: var(--color-primary-hover);
  background: var(--color-bg-elevated);
}
// components/Button/Button.jsx
import classNames from 'classnames';
import styles from './Button.module.css';

export function Button({ children, variant = 'primary', onClick, ...rest }) {
  return (
    <button
      className={classNames(styles.button, styles[variant])}
      onClick={onClick}
      {...rest}
    >
      {children}
    </button>
  );
}

8.5 Card 组件

/* components/Card/Card.module.css */
.card {
  background: var(--color-bg-elevated);
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  padding: 24px;
  box-shadow: 0 2px 8px var(--color-shadow);
  transition: var(--transition);
}

.card:hover {
  box-shadow: 0 4px 16px var(--color-shadow);
  transform: translateY(-2px);
}

.title {
  font-size: 18px;
  font-weight: 600;
  color: var(--color-text);
  margin: 0 0 8px 0;
}

.content {
  font-size: 14px;
  color: var(--color-text-muted);
  line-height: 1.6;
}
// components/Card/Card.jsx
import styles from './Card.module.css';

export function Card({ title, children }) {
  return (
    <div className={styles.card}>
      {title && <h3 className={styles.title}>{title}</h3>}
      <div className={styles.content}>{children}</div>
    </div>
  );
}

8.6 主题切换按钮

// components/ThemeToggle/ThemeToggle.jsx
import { useTheme } from '../../theme/ThemeContext';
import { Button } from '../Button/Button';

export function ThemeToggle() {
  const { theme, toggleTheme } = useTheme();

  return (
    <Button variant="outlined" onClick={toggleTheme}>
      {theme === 'light' ? '🌙 切换暗色' : '☀️ 切换亮色'}
    </Button>
  );
}

8.7 组装应用

// App.jsx
import { ThemeProvider } from './theme/ThemeContext';
import { ThemeToggle } from './components/ThemeToggle/ThemeToggle';
import { Button } from './components/Button/Button';
import { Card } from './components/Card/Card';
import './theme/theme.css';

function App() {
  return (
    <ThemeProvider>
      <div style={{
        minHeight: '100vh',
        background: 'var(--color-bg)',
        padding: '40px',
        transition: 'var(--transition)',
      }}>
        <div style={{ maxWidth: '600px', margin: '0 auto' }}>
          <div style={{
            display: 'flex',
            justifyContent: 'space-between',
            alignItems: 'center',
            marginBottom: '32px',
          }}>
            <h1 style={{ color: 'var(--color-text)', margin: 0 }}>
              组件库演示
            </h1>
            <ThemeToggle />
          </div>

          <Card title="样式方案总结">
            本章介绍了 React 中五种主流的样式方案:内联样式、CSS Modules、
            styled-components、Emotion 和 Tailwind CSS。每种方案都有各自的
            适用场景,没有绝对的好坏之分。
          </Card>

          <div style={{ marginTop: '24px', display: 'flex', gap: '12px' }}>
            <Button variant="primary">主要按钮</Button>
            <Button variant="danger">危险按钮</Button>
            <Button variant="outlined">描边按钮</Button>
          </div>

          <div style={{ marginTop: '24px' }}>
            <Card title="主题系统原理">
              通过 CSS 变量定义所有颜色值,切换主题时只需要切换根元素的
              data-theme 属性,所有子元素的样式会自动更新——这就是 CSS
              变量的级联特性带来的天然优势。
            </Card>
          </div>
        </div>
      </div>
    </ThemeProvider>
  );
}

export default App;

点击主题切换按钮,整个页面的背景色、文字颜色、卡片样式、按钮样式会同时平滑切换——而我们没有在任何组件里写 if (theme === 'dark') 这样的条件判断。CSS 变量的级联机制帮我们自动完成了一切。

这就是本章的核心思想:选对样式方案解决隔离问题,用 CSS 变量解决主题问题,两者正交组合,互不干扰


📝 掌握度自测

完成本章学习后,试着回答以下问题来检验掌握程度:

  1. CSS Modules 是如何实现样式隔离的? 它在编译时对 className 做了什么处理?为什么文件要命名为 *.module.css

  2. styled-components 中 &&& 有什么区别? 什么场景下你需要用 &&

  3. Tailwind CSS 最终生成的 CSS 文件会很大吗? 它是如何保证产物体积可控的?

  4. 用 CSS 变量实现主题切换时,为什么我们把变量定义在 :root[data-theme='dark'] 上,而不是通过 React 的 state 传递颜色值给每个组件? 从性能角度分析这两种方案的区别。

  5. 如果你要为一个新的 React 项目选择样式方案,你会考虑哪些因素? 请列出至少四个维度,并给出你的选型结论。

💡 自我评估

  • 能答对 1-2 题:已经掌握基础概念,建议动手用每种方案分别实现一个组件来加深理解
  • 能答对 3-4 题:理解比较深入,可以开始在项目中实践主题系统了
  • 全部答对:对 React 样式体系有系统认知,可以为团队做技术选型和架构设计了

购买课程解锁全部内容

从组件到架构:12 章系统掌握现代 React

¥29.90