目 录CONTENT

文章目录

React(四)React 中 CSS 使用全方位指南

React(四):React 中 CSS 使用全方位指南

目录


环境搭建前置说明

在开始学习之前,请确保你的开发环境已经准备就绪。推荐使用以下任一方式创建 React 项目:

方式一:Create React App(CRA)

# 创建项目
npx create-react-app my-react-app
cd my-react-app

# 启动开发服务器
npm start

CRA 内置了对 CSS Modules 的支持(文件名需以 .module.css 结尾),无需额外配置。

方式二:Vite

# 创建项目
npm create vite@latest my-react-app -- --template react
cd my-react-app
npm install
npm run dev

Vite 同样内置支持 CSS Modules,且构建速度更快,推荐新项目使用。

项目目录结构预览

my-react-app/
├── src/
│   ├── components/          # 组件目录
│   ├── styles/              # 全局样式目录
│   ├── App.jsx              # 根组件
│   └── index.js/main.jsx    # 入口文件
├── package.json
└── ...

第一章:基础内联样式(Inline CSS)

1.1 核心概念

在 React 中,内联样式不是像原生 HTML 那样写一个 CSS 字符串,而是通过组件的 style 属性传入一个 JavaScript 对象。这个对象中的属性名对应 CSS 属性名,但使用 小驼峰命名法(camelCase)

通俗解释:小驼峰命名法就是第一个单词小写,后面每个单词首字母大写,比如 background-color 变成 backgroundColorfont-size 变成 fontSize

1.2 基础语法演示

示例 1:静态内联样式

// 01-inline-static.jsx —— 可直接复制运行
function StaticStyle() {
  return (
    <div
      style={{
        backgroundColor: "#4CAF50", // CSS 中的 background-color
        color: "white", // 文本颜色
        padding: "20px", // 内边距
        borderRadius: "8px", // CSS 中的 border-radius
        fontSize: "18px", // CSS 中的 font-size
        textAlign: "center", // CSS 中的 text-align
      }}
    >
      我是带内联样式的盒子
    </div>
  );
}

export default StaticStyle;

关键规则速记表

规则 说明 示例
style 接收 JS 对象 不是字符串,外层 {} 是 JSX 表达式,内层 {} 是对象字面量 style={{ color: 'red' }}
属性名用驼峰命名 去掉连字符,第二个单词起首字母大写 margin-topmarginTop
像素值可省略单位 值为数字时,React 自动追加 px width: 200width: 200px
字符串值照常写 非数值属性必须写字符串 color: 'red'display: 'flex'
多单词属性需加引号 使用 " 包裹 'animationDuration': '2s'

示例 2:样式对象的三种组织方式

// 02-inline-organization.jsx —— 可直接复制运行
function StyleOrganization() {
  // 方式一:对象直接写在 JSX 中(适合只有少量属性的场景)
  const boxStyle1 = {
    backgroundColor: "#2196F3",
    padding: 15, // 数字,React 自动加 px
    marginBottom: 10,
    color: "white",
  };

  // 方式二:提取样式对象到外部变量(推荐,更清晰)
  const boxStyle2 = {
    backgroundColor: "#FF9800",
    padding: 20,
    marginTop: 10, // 驼峰命名
    color: "white",
    borderRadius: 6,
  };

  return (
    <div>
      {/* 方式一 */}
      <div style={boxStyle1}>样式对象方式一</div>

      {/* 方式二:直接写对象字面量(注意两层花括号) */}
      <div
        style={{
          backgroundColor: "#4CAF50",
          padding: 20,
          color: "white",
          marginTop: 10,
        }}
      >
        样式对象方式二
      </div>

      {/* 方式三:使用外部变量 */}
      <div style={boxStyle2}>样式对象方式三</div>

      {/* 方式四:合并多个样式对象 */}
      <div style={{ ...boxStyle1, ...boxStyle2 }}>
        合并样式(后者属性会覆盖前者同名属性)
      </div>
    </div>
  );
}

export default StyleOrganization;

1.3 动态样式实战

内联样式最大的优势之一就是动态计算样式——根据组件状态(state)或父组件传来的属性(props)灵活改变外观。

示例 1:基于状态切换的背景色

// 03-dynamic-state.jsx —— 可直接复制运行
import { useState } from "react";

function StateDrivenStyle() {
  const [isActive, setIsActive] = useState(false);

  // 根据状态动态计算样式
  const containerStyle = {
    padding: "30px",
    textAlign: "center",
    // 关键:根据 isActive 状态切换背景色
    backgroundColor: isActive ? "#4CAF50" : "#f44336",
    color: "white",
    fontSize: "20px",
    borderRadius: "8px",
    cursor: "pointer",
    transition: "background-color 0.3s ease", // transition 不受限制,可以正常使用
    userSelect: "none",
  };

  return (
    <div style={containerStyle} onClick={() => setIsActive(!isActive)}>
      当前状态:{isActive ? "激活 🟢" : "未激活 🔴"}
      <br />
      <span
        style={{ fontSize: "14px", marginTop: "8px", display: "inline-block" }}
      >
        点击我切换状态
      </span>
    </div>
  );
}

export default StateDrivenStyle;

示例 2:根据 props 动态调整样式

// 04-dynamic-props.jsx —— 可直接复制运行
function DynamicButton({ size = "medium", variant = "default", children }) {
  // 根据 size prop 计算实际像素值
  const sizeMap = {
    small: { padding: "6px 12px", fontSize: 12 },
    medium: { padding: "10px 20px", fontSize: 16 },
    large: { padding: "14px 28px", fontSize: 20 },
  };

  // 根据 variant prop 计算配色
  const variantMap = {
    default: { backgroundColor: "#e0e0e0", color: "#333" },
    primary: { backgroundColor: "#1976D2", color: "white" },
    danger: { backgroundColor: "#D32F2F", color: "white" },
  };

  // 合并 size 和 variant 的样式
  const buttonStyle = {
    border: "none",
    borderRadius: "6px",
    cursor: "pointer",
    fontWeight: "bold",
    transition: "all 0.2s ease",
    // 关键:展开运算符合并两个对象
    ...sizeMap[size],
    ...variantMap[variant],
  };

  return <button style={buttonStyle}>{children || "按钮"}</button>;
}

// 使用示例
function App() {
  return (
    <div style={{ display: "flex", gap: "10px", padding: "20px" }}>
      {/* 小号默认按钮 */}
      <DynamicButton size="small">小按钮</DynamicButton>
      {/* 中号主要按钮 */}
      <DynamicButton size="medium" variant="primary">
        主要按钮
      </DynamicButton>
      {/* 大号危险按钮 */}
      <DynamicButton size="large" variant="danger">
        删除
      </DynamicButton>
    </div>
  );
}

export default App;

1.4 踩坑提示 & 易混淆知识点

坑 1:两层花括号傻傻分不清楚

<div style={{ color: "red" }} />

外层 {} 是 JSX 中嵌入 JavaScript 表达式的语法,内层 {} 是 JavaScript 对象字面量的语法。本质上相当于:

const myStyle = { color: "red" };
<div style={myStyle} />;

坑 2:CSS 属性名写错了

新手极易写成 background-color(HTML 习惯),React 中必须写 backgroundColor。常见的还有 margin-topmarginTopborder-radiusborderRadiusz-indexzIndex

坑 3:单位混淆

width: 200 在 React 中会被自动转为 width: 200px。但如果你真的需要百分比/em/rem/vh 等单位,必须写成字符串:width: '50%'fontSize: '1.5rem'

1.5 内联样式的局限性

不可使用的 CSS 特性 原因
伪类 :hover:active:focus:nth-child() 内联样式只有属性-值对,无法表示选择器
伪元素 ::before::after 同上
媒体查询 @media 无法写在 style 对象中
关键帧动画 @keyframes 同上(但 animation 属性本身可以引用外部定义的 keyframes)
后代选择器子选择器 内联样式只能作用于当前元素

权重(优先级)问题:内联样式的特异性等同于原生 HTML 内联样式,权重为 (1,0,0,0),几乎高于所有外部样式表中的选择器。这既是优势(确保样式一定生效),也是隐患(难以被外部样式覆盖)。

1.6 适用场景总结

适用 不太适用
需要根据 JS 变量动态计算的样式 全局统一的样式规范
量少且仅作用于单个元素的样式 需要伪类交互(hover 等)
快速原型开发、演示代码 复杂的响应式布局
动画库中动态修改的样式 团队协作的大型项目

本章思考练习题

  1. 下面代码有什么问题?请指出并修正:
    <div style="color: red; font-size: 16px;">Hello</div>
    
  2. 为什么要使用小驼峰命名法(camelCase)?z-index 在 React 内联样式中应该怎么写?
  3. 如何使用内联样式实现一个宽高为 200px 的红色正方形,并将其水平居中?
  4. 如果想在点击按钮时切换按钮的背景色,应该使用什么 React 机制配合内联样式实现?
  5. 内联样式能否实现一个在鼠标悬停时放大 1.2 倍的元素?如果不能,有什么替代方案?

第二章:外部 CSS 文件

React 支持像传统网页开发一样引入外部 .css 文件。但在此基础上,React 生态还提供了一种更强大的方案——CSS Modules,用于解决全局样式污染问题。

2.1 普通外部 CSS

核心概念

普通外部 CSS 就是大家熟悉的 .css 文件,在 React 中通过 import 语句引入。它的规则与传统 Web 开发完全一致——所有类名都是全局的

全局样式污染:同一个类名在不同组件中可能互相覆盖,就像两个人取了同样的名字,叫一声两个人都回头——你不知道影响的到底是哪一个。

基础用法

步骤 1:创建 CSS 文件

/* styles/global-button.css */
.btn {
  padding: 10px 24px;
  border: none;
  border-radius: 6px;
  font-size: 16px;
  font-weight: 600;
  cursor: pointer;
  transition: all 0.2s ease;
}

.btn-primary {
  background-color: #1976d2;
  color: #fff;
}

.btn-primary:hover {
  background-color: #1565c0;
  transform: translateY(-1px);
  box-shadow: 0 4px 12px rgba(25, 118, 210, 0.3);
}

.btn-danger {
  background-color: #d32f2f;
  color: #fff;
}

.btn-danger:hover {
  background-color: #b71c1c;
}

.btn-large {
  padding: 14px 36px;
  font-size: 20px;
}

.btn-small {
  padding: 4px 12px;
  font-size: 12px;
}

步骤 2:在组件中引入并使用

// 05-external-css.jsx —— 与上面的 CSS 文件配合使用
import "./styles/global-button.css"; // 直接 import CSS 文件

function ExternalCSSDemo() {
  return (
    <div
      style={{
        padding: "30px",
        display: "flex",
        gap: "12px",
        alignItems: "center",
      }}
    >
      {/* className 是 JSX 中对应 HTML class 属性的写法 */}
      <button className="btn btn-primary">普通按钮</button>
      <button className="btn btn-danger">危险按钮</button>
      {/* 多个类名用空格分隔 */}
      <button className="btn btn-primary btn-large">大号按钮</button>
      <button className="btn btn-primary btn-small">小号按钮</button>
    </div>
  );
}

export default ExternalCSSDemo;

重要提醒:JSX 中使用 className 而非 class。这是因为 class 是 JavaScript 的保留关键字,React 使用 className 来避免冲突。

类名命名规范——BEM 规范

为了避免全局样式污染,社区推荐使用 BEM(Block Element Modifier,块-元素-修饰符) 命名规范:

/* BEM 命名格式:block__element--modifier */

/* 块(Block):独立的组件 */
.header {
}

/* 元素(Element):块的子元素,用双下划线连接 */
.header__logo {
}
.header__nav {
}

/* 修饰符(Modifier):块或元素的变体,用双连字符连接 */
.header--dark {
}
.header__nav--collapsed {
}
.btn--disabled {
}

BEM 实战示例:

/* styles/card.css —— BEM 命名示范 */
.card {
  border: 1px solid #e0e0e0;
  border-radius: 12px;
  overflow: hidden;
  transition: box-shadow 0.3s;
}

.card:hover {
  box-shadow: 0 6px 24px rgba(0, 0, 0, 0.1);
}

.card__image {
  width: 100%;
  height: 200px;
  object-fit: cover;
}

.card__body {
  padding: 16px;
}

.card__title {
  font-size: 18px;
  font-weight: 700;
  margin: 0 0 8px;
}

.card__description {
  color: #666;
  font-size: 14px;
  line-height: 1.5;
}

/* 修饰符:不同类型的卡片变体 */
.card--featured {
  border-color: #1976d2;
  border-width: 2px;
}

.card--horizontal {
  display: flex;
}

.card--horizontal .card__image {
  width: 200px;
  height: auto;
}
// 06-bem-card.jsx —— BEM 实战
import "./styles/card.css";

function Card({ title, description, imageUrl, featured, horizontal }) {
  // 动态拼接类名
  const cardClass = [
    "card",
    featured && "card--featured", // 条件类名
    horizontal && "card--horizontal", // 条件类名
  ]
    .filter(Boolean) // 过滤掉 false/null/undefined
    .join(" "); // 用空格连接

  return (
    <div className={cardClass}>
      <img className="card__image" src={imageUrl} alt={title} />
      <div className="card__body">
        <h3 className="card__title">{title}</h3>
        <p className="card__description">{description}</p>
      </div>
    </div>
  );
}

export default Card;

2.2 CSS Modules

核心概念

CSS Modules 是 React 社区为解决"全局样式污染"而生的方案。它的核心思想是:将每个 CSS 文件中的类名进行编译(哈希化),生成全局唯一的类名,从而实现样式隔离

通俗类比:普通 CSS 像公共广播(谁都听得到),CSS Modules 像对讲机(只有特定的人能听到)。

命名规则与使用方式

步骤 1:创建 CSS Module 文件

文件必须以 .module.css 结尾,例如 Button.module.css

/* Button.module.css */

/* 在 CSS Module 中,类名可以随意取,不必担心全局冲突 */
.button {
  padding: 10px 24px;
  border: none;
  border-radius: 6px;
  font-size: 16px;
  font-weight: 600;
  cursor: pointer;
  transition: all 0.2s ease;
}

.primary {
  background-color: #1976d2;
  color: #ffffff;
}

.primary:hover {
  background-color: #1565c0;
}

.danger {
  background-color: #d32f2f;
  color: #ffffff;
}

.large {
  padding: 14px 36px;
  font-size: 20px;
}

/* 可以使用 :global 声明全局样式(谨慎使用) */
:global(.reset-margin) {
  margin: 0;
}

步骤 2:在组件中使用

// 07-css-modules.jsx —— 可直接复制运行
// 注意:import 后得到一个包含所有类名的对象
import styles from "./Button.module.css";

function CSSModulesButton({ variant = "primary", size, children }) {
  // styles 对象结构示例(编译后):
  // {
  //   button: 'Button_button__a1b2c',
  //   primary: 'Button_primary__d3e4f',
  //   danger: 'Button_danger__g5h6i',
  //   large: 'Button_large__j7k8l'
  // }

  return (
    <button
      className={[
        styles.button, // 基础样式
        styles[variant], // 动态取变体样式(key 就是 CSS 中的类名)
        size === "large" && styles.large, // 条件类名
      ]
        .filter(Boolean)
        .join(" ")}
    >
      {children}
    </button>
  );
}

export default CSSModulesButton;

动态类名拼接技巧

// 08-css-modules-advance.jsx —— 动态类名处理
import styles from "./Card.module.css";

function Card({ type, isActive, isDisabled }) {
  // 技巧 1:使用数组 + filter + join(推荐,最灵活)
  const cardClass1 = [
    styles.card,
    styles[type], // 动态取对应类名
    isActive && styles.active, // 条件类名
    isDisabled && styles.disabled,
  ]
    .filter(Boolean)
    .join(" ");

  // 技巧 2:使用模板字符串(适合只有两个类名的情况)
  const cardClass2 = `${styles.card} ${isActive ? styles.active : ""}`.trim();

  // 技巧 3:使用 classnames 库(需要安装:npm install classnames)
  // import classNames from 'classnames';
  // const cardClass3 = classNames({
  //   [styles.card]: true,
  //   [styles.active]: isActive,
  //   [styles.disabled]: isDisabled,
  // });

  return <div className={cardClass1}>{/* 卡片内容 */}</div>;
}

export default Card;

CSS Modules 编译原理演示

假设你写了以下 CSS Modules 文件:

/* Demo.module.css */
.title {
  font-size: 20px;
}
.container {
  display: flex;
}

经过构建工具(Webpack / Vite)编译后,实际输出的 CSS 会变成:

/* 编译后:类名被自动加上了哈希后缀 */
.Demo_title__3xk9f {
  font-size: 20px;
}
.Demo_container__a7b2m {
  display: flex;
}

而你 import 时拿到的 JS 对象则映射了原始类名到编译后类名的关系:

// import styles from './Demo.module.css' 的实际效果
const styles = {
  title: "Demo_title__3xk9f",
  container: "Demo_container__a7b2m",
};

这就是 CSS Modules 实现样式隔离的核心原理——你的 .title 在最终 HTML 中变成了独一无二的 .Demo_title__3xk9f,不会和任何其他组件的 .title 冲突。

2.3 两种外部 CSS 方案的对比

维度 普通外部 CSS CSS Modules
类名作用域 全局,有污染风险 局部,自动隔离
学习成本 零,传统 CSS 写法 极低,只需了解 import 新语法
配置成本 零配置 CRA / Vite 内置支持
动态样式 配合 className 拼接 同上,且对象访问更直观
团队协作 需要严格命名规范(BEM) 天然隔离,命名更自由
类型支持 可配合 TypeScript 类型声明

本章思考练习题

  1. JSX 中为什么用 className 而不是 class?两者在使用上有什么区别?
  2. CSS Modules 的文件命名有什么特殊要求?如果命名为 styles.css 而非 styles.module.css,会发生什么?
  3. 在 CSS Modules 中,如果想让某个类名保持全局可见(不被哈希化),应该使用什么语法?
  4. 请解释 BEM 命名规范中 __-- 分别代表什么含义。使用 BEM 规范的根本目的是什么?
  5. 普通外部 CSS 和 CSS Modules 在动态类名拼接上的写法有什么异同?哪种更适合多人协作的大型项目?

第三章:CSS-in-JS 方案

CSS-in-JS 是指在 JavaScript 中编写 CSS 样式的方案。它不是某一种库,而是一种思路——把样式也当作 JavaScript 变量/组件来处理。

通俗理解:传统开发中 CSS 和 JS 是分离的两个世界,CSS-in-JS 则打破了这道墙——你在写 JS 的同一个文件中就能定义和控制样式,让样式也能享受 JS 的变量、函数、条件判断等全部能力。

3.1 styled-components

styled-components 是目前最流行、社区最大的 CSS-in-JS 库。它的核心思路是:一个样式化的组件

安装

npm install styled-components

如果项目使用 TypeScript,还需安装类型声明(新版本已内置,一般不需要单独安装):

npm install -D @types/styled-components

基础语法:创建样式化组件

// 09-styled-basic.jsx —— 可直接复制运行
import styled from "styled-components";

// 创建一个带样式的 div 组件
const Card = styled.div`
  background-color: #ffffff;
  border: 1px solid #e0e0e0;
  border-radius: 12px;
  padding: 24px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
  transition: box-shadow 0.3s ease;
  /* hover 伪类直接写在模板字符串中 */
  &:hover {
    box-shadow: 0 6px 20px rgba(0, 0, 0, 0.12);
  }
`;

// 创建带样式的 h2 组件
const Title = styled.h2`
  font-size: 1.5rem;
  color: #333;
  margin: 0 0 12px 0;
`;

// 创建带样式的 p 组件
const Description = styled.p`
  font-size: 0.95rem;
  color: #666;
  line-height: 1.6;
  margin: 0;
`;

function StyledComponentsDemo() {
  return (
    <Card>
      <Title>卡片标题</Title>
      <Description>
        styled-components 让你用写 CSS 的方式写组件样式。
        &:hover、媒体查询、动画等都能使用,语法和原生 CSS 完全一致。
      </Description>
    </Card>
  );
}

export default StyledComponentsDemo;

传递 Props 实现动态样式

这是 styled-components 的核心优势之一——组件接收的 props 可以直接用于样式计算

// 10-styled-props.jsx —— Props 驱动的动态样式
import styled from "styled-components";

// 根据 $variant prop 动态改变样式
// 注意:使用 $ 前缀的 transient prop 不会传递到 DOM 元素(React 18+ 推荐)
const StyledButton = styled.button`
  /* 基础样式 */
  padding: 10px 24px;
  border: none;
  border-radius: 8px;
  font-size: 16px;
  font-weight: 600;
  cursor: pointer;
  transition: all 0.2s ease;

  /* 根据 $variant prop 动态计算背景色和文字色 */
  background-color: ${({ $variant }) => {
    switch ($variant) {
      case "primary":
        return "#1976D2";
      case "danger":
        return "#D32F2F";
      case "success":
        return "#388E3C";
      default:
        return "#e0e0e0";
    }
  }};

  color: ${({ $variant }) => ($variant === "default" ? "#333" : "#ffffff")};

  /* 根据 $size prop 动态调整尺寸 */
  padding: ${({ $size }) => {
    switch ($size) {
      case "small":
        return "6px 14px";
      case "large":
        return "16px 36px";
      default:
        return "10px 24px";
    }
  }};

  font-size: ${({ $size }) => {
    switch ($size) {
      case "small":
        return "0.8rem";
      case "large":
        return "1.2rem";
      default:
        return "1rem";
    }
  }};

  /* 伪类 hover 样式(也支持动态样式) */
  &:hover {
    transform: translateY(-1px);
    box-shadow: 0 4px 12px
      ${({ $variant }) =>
        $variant === "primary"
          ? "rgba(25, 118, 210, 0.3)"
          : $variant === "danger"
            ? "rgba(211, 47, 47, 0.3)"
            : "rgba(0, 0, 0, 0.1)"};
  }

  /* 伪类 active */
  &:active {
    transform: translateY(0);
  }

  /* 禁用状态 */
  &:disabled {
    opacity: 0.5;
    cursor: not-allowed;
  }
`;

function PropsDemo() {
  return (
    <div
      style={{
        display: "flex",
        gap: "12px",
        flexWrap: "wrap",
        padding: "20px",
      }}
    >
      <StyledButton $variant="primary">主要按钮</StyledButton>
      <StyledButton $variant="danger">危险按钮</StyledButton>
      <StyledButton $variant="success">成功按钮</StyledButton>
      <StyledButton $variant="default">默认按钮</StyledButton>
      <StyledButton $variant="primary" $size="large">
        大号按钮
      </StyledButton>
      <StyledButton $variant="primary" $size="small">
        小号按钮
      </StyledButton>
      <StyledButton $variant="primary" disabled>
        禁用按钮
      </StyledButton>
    </div>
  );
}

export default PropsDemo;

媒体查询 & 嵌套语法

// 11-styled-responsive.jsx —— 响应式 + 嵌套
import styled from "styled-components";

const ResponsiveCard = styled.div`
  background: #fff;
  border-radius: 12px;
  padding: 24px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);

  /* 嵌套子元素 */
  .card-header {
    font-size: 1.4rem;
    font-weight: 700;
    margin-bottom: 12px;
  }

  .card-body {
    color: #555;
    line-height: 1.7;
  }

  /* 媒体查询 —— 语法和原生 CSS 完全一样 */
  @media (max-width: 768px) {
    padding: 16px;

    .card-header {
      font-size: 1.1rem;
    }

    .card-body {
      font-size: 0.9rem;
    }
  }

  @media (max-width: 480px) {
    padding: 12px;
    border-radius: 8px;
  }
`;

export default ResponsiveCard;

全局样式注入

// 12-global-style.jsx —— 全局样式
import { createGlobalStyle } from "styled-components";

const GlobalStyle = createGlobalStyle`
  /* CSS Reset 或全局基础样式 */
  *, *::before, *::after {
    box-sizing: border-box;
    margin: 0;
    padding: 0;
  }

  body {
    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
      'Helvetica Neue', Arial, sans-serif;
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
    background-color: #f5f5f5;
    color: #333;
  }

  a {
    text-decoration: none;
    color: inherit;
  }
`;

// 在根组件中使用(放在最顶层)
function App() {
  return (
    <>
      <GlobalStyle />
      {/* 其他组件 */}
    </>
  );
}

export default App;

实战案例:完整主题化按钮组件

// 13-styled-theme.jsx —— 企业级按钮组件(完整可运行)
import styled, { ThemeProvider } from "styled-components";

// ==================== 1. 定义主题对象 ====================
const lightTheme = {
  colors: {
    primary: "#1976D2",
    primaryHover: "#1565C0",
    danger: "#D32F2F",
    dangerHover: "#B71C1C",
    success: "#388E3C",
    successHover: "#2E7D32",
    text: "#333",
    textOnPrimary: "#fff",
  },
  radii: {
    sm: "4px",
    md: "8px",
    lg: "12px",
  },
};

const darkTheme = {
  colors: {
    primary: "#64B5F6",
    primaryHover: "#42A5F5",
    danger: "#EF5350",
    dangerHover: "#E53935",
    success: "#66BB6A",
    successHover: "#4CAF50",
    text: "#f5f5f5",
    textOnPrimary: "#1a1a1a",
  },
  radii: {
    sm: "4px",
    md: "8px",
    lg: "12px",
  },
};

// ==================== 2. 创建主题化按钮组件 ====================
const ThemeButton = styled.button`
  /* 基础样式 */
  padding: ${({ $size }) =>
    $size === "large"
      ? "14px 36px"
      : $size === "small"
        ? "6px 14px"
        : "10px 24px"};
  font-size: ${({ $size }) =>
    $size === "large" ? "1.2rem" : $size === "small" ? "0.8rem" : "1rem"};
  font-weight: 600;
  border: none;
  border-radius: ${({ theme }) => theme.radii.md};
  cursor: pointer;
  transition: all 0.2s ease;

  /* 通过 theme 对象获取颜色(核心用法) */
  background-color: ${({ $variant, theme }) =>
    theme.colors[$variant] || theme.colors.primary};
  color: ${({ theme }) => theme.colors.textOnPrimary};

  /* hover 态 */
  &:hover {
    background-color: ${({ $variant, theme }) => {
      const hoverKey = $variant + "Hover";
      return theme.colors[hoverKey] || theme.colors.primaryHover;
    }};
    transform: translateY(-1px);
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
  }

  &:active {
    transform: translateY(0);
  }

  /* 响应式:移动端按钮更宽,方便手指点击 */
  @media (max-width: 480px) {
    width: 100%;
    padding: 12px 0;
    text-align: center;
  }
`;

// ==================== 3. 应用入口 ====================
function App() {
  const [isDark, setIsDark] = useState(false);

  return (
    <ThemeProvider theme={isDark ? darkTheme : lightTheme}>
      <div style={{ padding: "40px", textAlign: "center" }}>
        <button onClick={() => setIsDark(!isDark)}>
          切换为{isDark ? "浅色" : "深色"}主题
        </button>
        <div
          style={{
            display: "flex",
            gap: "12px",
            marginTop: "20px",
            justifyContent: "center",
          }}
        >
          <ThemeButton $variant="primary">主要按钮</ThemeButton>
          <ThemeButton $variant="danger">危险按钮</ThemeButton>
          <ThemeButton $variant="success">成功按钮</ThemeButton>
          <ThemeButton $variant="primary" $size="large">
            大号按钮
          </ThemeButton>
        </div>
      </div>
    </ThemeProvider>
  );
}

export default App;

styled-components 核心原理

运行时渲染:styled-components 在 JavaScript 运行时将模板字符串中的 CSS 解析为真正的 <style> 标签插入到 HTML 的 <head> 中。每次组件的 props 变化时,它重新计算样式并更新。

这种"运行时"机制的好处是动态性极强,坏处是会增加 JS bundle 体积和首次渲染时间。


3.2 Emotion(拓展)

Emotion 是另一个流行的 CSS-in-JS 库,思路与 styled-components 类似,但更轻量、语法更灵活。它提供两种主要用法:styled API(与 styled-components 兼容)和 css prop API。

安装

npm install @emotion/react @emotion/styled

styled API(与 styled-components 几乎相同)

/** @jsxImportSource @emotion/react */
import styled from "@emotion/styled";

const EmotionButton = styled.button`
  padding: 10px 24px;
  background-color: ${({ variant }) =>
    variant === "primary" ? "#1976D2" : "#e0e0e0"};
  color: ${({ variant }) => (variant === "primary" ? "#fff" : "#333")};
  border: none;
  border-radius: 6px;
  cursor: pointer;

  &:hover {
    opacity: 0.9;
  }
`;

// 使用方式完全一样,直接 <EmotionButton variant="primary">点我</EmotionButton>

css prop API(Emotion 独有特色)

/** @jsxImportSource @emotion/react */
// 注意:文件顶部必须有 /** @jsxImportSource @emotion/react */ 这行 pragma 注释
import { css } from "@emotion/react";

function EmotionDemo() {
  const baseStyle = css`
    padding: 12px 20px;
    border-radius: 8px;
  `;

  const primaryStyle = css`
    background-color: #1976d2;
    color: white;
    &:hover {
      background-color: #1565c0;
    }
  `;

  // css prop 可以直接传入样式,支持组合
  return <button css={[baseStyle, primaryStyle]}>Emotion css prop 方式</button>;
}

styled-components vs Emotion 对比

维度 styled-components Emotion
包体积 约 12KB gzip 约 7KB gzip
语法 仅 styled API styled + css prop 双 API
社区生态 更大,教程更多 灵活但文档稍少
React 18 兼容 完美 完美
推荐场景 大多数项目 追求轻量与灵活性的项目

3.3 CSS-in-JS 的适用场景与权衡

优点:

  • 完全消除类名冲突(自动生成唯一类名)
  • 动态样式能力极强(props、state 直接驱动样式)
  • 样式与组件逻辑天然内聚,便于维护和理解
  • 支持嵌套、伪类、媒体查询等全部 CSS 特性
  • 可以享用 JS 生态的全部工具(TypeScript、lint、prettier 等)

缺点:

  • 增加运行时开销(样式计算在浏览器中进行)
  • JS Bundle 体积增加
  • SSR 需要额外配置(Next.js 中需要 babel-plugin-styled-components)
  • 调试时类名是哈希值,不如普通 CSS 直观
  • 对 CSS 传统开发者有学习成本

SSR 注意:在 Next.js 等 SSR 框架中使用 styled-components,需要配置 babel-plugin-styled-components 并补充 _document.js 中的服务端样式注入逻辑。


本章思考练习题

  1. CSS-in-JS 中的 "J" 指的是什么?它解决的核心痛点是什么?
  2. styled-components 中为什么使用 & 符号表示伪类和嵌套选择器?
  3. 请写出一个 styled-components 的代码片段:创建一个 Box 组件,接收 $bgColor prop 控制背景色,当 $bgColor 不传时默认为 #f0f0f0
  4. Emotion 相比 styled-components 有什么独特的 API?什么场景下更适合选择 Emotion?
  5. CSS-in-JS 方案在 Next.js 的 SSR 模式下可能遇到什么问题?为什么?

第四章:Tailwind CSS 原子化 CSS

4.1 原子化 CSS 的核心理念

Tailwind CSS 是一种"原子化 CSS(Utility-First CSS)"框架。其核心思想是:提供海量的单一职责 CSS 工具类,通过组合这些工具类来构建界面,而不是写自定义 CSS

通俗比喻:传统写 CSS 就像从零开始做菜(自己切菜、调味、摆盘),Tailwind 则像用乐高积木搭建——每个工具类就是一个积木块,你只需要把它们组合起来。

<!-- 传统方式:写自定义类 -->
<button class="my-custom-btn">按钮</button>

<!-- Tailwind 方式:组合原子工具类 -->
<button class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700">
  按钮
</button>

4.2 安装与配置

方式一:Create React App

# 1. 安装 Tailwind 及其依赖
npm install -D tailwindcss postcss autoprefixer

# 2. 生成配置文件
npx tailwindcss init -p

修改生成的 tailwind.config.js

// tailwind.config.js
/** @type {import('tailwindcss').Config} */
module.exports = {
  content: [
    "./src/**/*.{js,jsx,ts,tsx}", // 告诉 Tailwind 扫描哪些文件
  ],
  theme: {
    extend: {},
  },
  plugins: [],
};

src/index.css 顶部加入 Tailwind 指令:

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

方式二:Vite(推荐新项目)

# 1. 安装 Tailwind
npm install -D tailwindcss @tailwindcss/vite

修改 vite.config.js

// vite.config.js
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import tailwindcss from "@tailwindcss/vite";

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

在入口 CSS 文件中引入 Tailwind:

/* src/index.css */
@import "tailwindcss";

4.3 基础用法演示

常用工具类速查

分类 工具类前缀 示例
布局 flex, grid, block flex items-center justify-between
间距 p-, m-, gap- p-4(padding:1rem)、mx-auto(水平居中)
尺寸 w-, h-, max-w- w-full(width:100%)、h-64(height:16rem)
颜色 bg-, text-, border- bg-blue-500text-whiteborder-gray-200
圆角 rounded- rounded-lg(0.5rem)、rounded-full(完全圆形)
阴影 shadow- shadow-mdshadow-xl
字体 font-, text- font-boldtext-lgtext-center
过渡 transition-, duration- transition-all duration-300

示例 1:卡片组件

// 14-tailwind-card.jsx —— 可直接复制运行
function TailwindCard({ title, description, imageUrl }) {
  return (
    <div className="max-w-sm mx-auto bg-white rounded-xl shadow-md overflow-hidden hover:shadow-xl transition-shadow duration-300">
      {/* 图片区域 */}
      <img className="w-full h-48 object-cover" src={imageUrl} alt={title} />
      {/* 内容区域 */}
      <div className="p-6">
        <h3 className="text-xl font-bold text-gray-800 mb-2">{title}</h3>
        <p className="text-gray-600 text-sm leading-relaxed">{description}</p>
        {/* 底部按钮 */}
        <button className="mt-4 px-4 py-2 bg-blue-600 text-white font-semibold rounded-lg hover:bg-blue-700 active:bg-blue-800 transition-colors duration-200">
          了解更多
        </button>
      </div>
    </div>
  );
}

export default TailwindCard;

示例 2:响应式前缀

响应式前缀格式为 {断点}:{类名},从小屏幕开始逐步覆盖:

// 15-tailwind-responsive.jsx
function ResponsiveBox() {
  return (
    <div
      className="
        w-full                    /* 默认(移动端):宽度 100% */
        sm:w-3/4                  /* >=640px:宽度 75% */
        md:w-1/2                  /* >=768px:宽度 50% */
        lg:w-1/3                  /* >=1024px:宽度 33% */
        mx-auto                   /* 水平居中 */
        p-4 sm:p-6 lg:p-8         /* 不同屏幕的内边距 */
        bg-blue-100 sm:bg-green-100 md:bg-yellow-100 lg:bg-purple-100  /* 不同大小不同背景色 */
        text-sm md:text-base lg:text-lg  /* 字体大小响应式 */
        rounded-md lg:rounded-xl
      "
    >
      <p className="text-center">调整浏览器宽度看我的变化!</p>
    </div>
  );
}

export default ResponsiveBox;

示例 3:状态前缀(hover / focus / active 等)

// 16-tailwind-states.jsx —— 各种交互状态
function InteractiveInput() {
  return (
    <div className="max-w-md mx-auto space-y-4 p-6">
      {/* hover 态按钮 */}
      <button
        className="
          px-6 py-3 bg-indigo-500 text-white font-semibold rounded-lg
          hover:bg-indigo-600           /* 悬停:背景加深 */
          hover:scale-105                /* 悬停:放大 5% */
          active:scale-95                /* 按下:略微缩小 */
          focus:outline-none             /* 聚焦:去掉默认轮廓 */
          focus:ring-2 focus:ring-indigo-300  /* 聚焦:显示光环 */
          transition-all duration-200    /* 平滑过渡 */
        "
      >
        交互按钮
      </button>

      {/* focus 态输入框 */}
      <input
        type="text"
        placeholder="聚焦我试试"
        className="
          w-full px-4 py-3 border-2 border-gray-300 rounded-lg
          focus:border-blue-500          /* 聚焦:边框变蓝 */
          focus:outline-none             /* 聚焦:去掉默认轮廓 */
          focus:ring-2 focus:ring-blue-200  /* 聚焦:蓝色光环 */
          placeholder:text-gray-400      /* 占位文字颜色 */
          transition-all duration-200
        "
      />

      {/* disabled 态 */}
      <button
        disabled
        className="
          px-6 py-3 bg-gray-400 text-white font-semibold rounded-lg
          cursor-not-allowed opacity-50
        "
      >
        禁用状态
      </button>
    </div>
  );
}

export default InteractiveInput;

4.4 自定义主题扩展

// tailwind.config.js —— 自定义主题
module.exports = {
  content: ["./src/**/*.{js,jsx,ts,tsx}"],
  theme: {
    extend: {
      // 扩展自定义颜色
      colors: {
        brand: {
          50: "#eef2ff",
          100: "#e0e7ff",
          500: "#6366f1", // 主品牌色
          600: "#4f46e5",
          700: "#4338ca",
        },
      },
      // 扩展自定义间距
      spacing: {
        128: "32rem",
        144: "36rem",
      },
      // 扩展自定义字体大小
      fontSize: {
        xxs: "0.625rem",
      },
      // 扩展自定义动画
      animation: {
        "fade-in": "fadeIn 0.5s ease-out",
      },
      keyframes: {
        fadeIn: {
          "0%": { opacity: "0", transform: "translateY(-10px)" },
          "100%": { opacity: "1", transform: "translateY(0)" },
        },
      },
    },
  },
  plugins: [],
};

使用自定义工具类:

<div className="bg-brand-500 text-brand-50 hover:bg-brand-600">
  品牌色按钮
</div>

<div className="animate-fade-in">
  淡入动画
</div>

4.5 实战案例:响应式导航栏

// 17-tailwind-navbar.jsx —— 完整可运行的响应式导航栏
import { useState } from "react";

function Navbar() {
  const [isOpen, setIsOpen] = useState(false);
  const [isScrolled, setIsScrolled] = useState(false);

  // 监听滚动:超过 50px 时改变导航栏样式
  if (typeof window !== "undefined") {
    window.onscroll = () => {
      setIsScrolled(window.scrollY > 50);
    };
  }

  return (
    <nav
      className={`
        sticky top-0 z-50
        transition-all duration-300
        ${
          isScrolled
            ? "bg-white shadow-lg backdrop-blur-sm bg-white/90"
            : "bg-transparent"
        }
      `}
    >
      <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
        <div className="flex justify-between items-center h-16">
          {/* Logo */}
          <div className="flex-shrink-0">
            <span className="text-2xl font-bold text-indigo-600 cursor-pointer">
              MyLogo
            </span>
          </div>

          {/* 桌面端菜单 */}
          <div className="hidden md:flex items-center space-x-8">
            <a
              href="#"
              className="text-gray-700 hover:text-indigo-600 transition-colors font-medium"
            >
              首页
            </a>
            <a
              href="#"
              className="text-gray-700 hover:text-indigo-600 transition-colors font-medium"
            >
              产品
            </a>
            <a
              href="#"
              className="text-gray-700 hover:text-indigo-600 transition-colors font-medium"
            >
              关于
            </a>
            <a
              href="#"
              className="text-gray-700 hover:text-indigo-600 transition-colors font-medium"
            >
              联系
            </a>
            <button className="px-5 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition-colors font-semibold">
              立即体验
            </button>
          </div>

          {/* 移动端汉堡按钮 */}
          <div className="md:hidden">
            <button
              onClick={() => setIsOpen(!isOpen)}
              className="text-gray-700 hover:text-indigo-600 focus:outline-none p-2"
            >
              {/* 汉堡图标:三条线 */}
              <svg
                className="w-6 h-6"
                fill="none"
                stroke="currentColor"
                viewBox="0 0 24 24"
              >
                {isOpen ? (
                  <path
                    strokeLinecap="round"
                    strokeLinejoin="round"
                    strokeWidth={2}
                    d="M6 18L18 6M6 6l12 12"
                  />
                ) : (
                  <path
                    strokeLinecap="round"
                    strokeLinejoin="round"
                    strokeWidth={2}
                    d="M4 6h16M4 12h16M4 18h16"
                  />
                )}
              </svg>
            </button>
          </div>
        </div>
      </div>

      {/* 移动端折叠菜单 */}
      <div
        className={`
          md:hidden overflow-hidden transition-all duration-300 ease-in-out
          ${isOpen ? "max-h-96 opacity-100" : "max-h-0 opacity-0"}
        `}
      >
        <div className="px-4 py-3 space-y-2 bg-white border-t border-gray-100">
          <a
            href="#"
            className="block px-3 py-2 rounded-lg text-gray-700 hover:bg-indigo-50 hover:text-indigo-600 transition-colors"
          >
            首页
          </a>
          <a
            href="#"
            className="block px-3 py-2 rounded-lg text-gray-700 hover:bg-indigo-50 hover:text-indigo-600 transition-colors"
          >
            产品
          </a>
          <a
            href="#"
            className="block px-3 py-2 rounded-lg text-gray-700 hover:bg-indigo-50 hover:text-indigo-600 transition-colors"
          >
            关于
          </a>
          <a
            href="#"
            className="block px-3 py-2 rounded-lg text-gray-700 hover:bg-indigo-50 hover:text-indigo-600 transition-colors"
          >
            联系
          </a>
          <button className="w-full mt-2 px-5 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition-colors font-semibold">
            立即体验
          </button>
        </div>
      </div>
    </nav>
  );
}

export default Navbar;

4.6 搭配 clsx 实现动态类名

当条件类名较多时,手动拼接字符串非常繁琐。推荐搭配 clsx 库(仅 228 字节):

npm install clsx
// 18-tailwind-clsx.jsx
import clsx from "clsx";

function StatusBadge({ status }) {
  return (
    <span
      className={clsx(
        // 基础类名(始终存在)
        "inline-flex items-center px-3 py-1 rounded-full text-sm font-medium",
        // 条件类名(根据 status 切换)
        {
          "bg-green-100 text-green-800": status === "success",
          "bg-red-100 text-red-800": status === "error",
          "bg-yellow-100 text-yellow-800": status === "warning",
          "bg-blue-100 text-blue-800": status === "info",
        },
      )}
    >
      {status}
    </span>
  );
}

// 用法:<StatusBadge status="success" />
export default StatusBadge;

4.7 Tree-shaking 机制

Tailwind CSS 通过扫描源码中的类名,只生成实际使用到的 CSS:

  1. 开发阶段:Tailwind 按需生成 CSS,但在开发模式下仍会生成所有工具类以方便调试
  2. 生产构建:根据 content 配置扫描 src/ 下所有文件,只保留实际使用的工具类,未使用的会被"摇掉"(tree-shake)
  3. 最终体积:一个典型的 Tailwind 项目最终 CSS 体积约 3-10KB(gzip 后)

4.8 Tailwind 的优缺点

优点:

  • 极快的原型开发速度,样式直接在 HTML/JSX 上写
  • 生产环境 CSS 体积极小(Tree-shaking)
  • 响应式设计极其便利(sm:/md:/lg: 前缀)
  • 团队协作风格统一(预设的工具类就是设计规范)
  • 无命名烦恼

缺点:

  • 学习曲线:需要记忆大量工具类名(初期可借助 VS Code 插件 Tailwind CSS IntelliSense 自动补全)
  • JSX 中类名很长,看起来有些拥挤
  • 从 0 搭建大型项目时,建议先定制 tailwind.config.js 中的主题,而非完全使用默认值
  • 不适合需要极端像素级还原的特殊设计稿

本章思考练习题

  1. Tailwind CSS 的"原子化 CSS"是什么意思?它和传统写自定义类的方式有什么本质不同?
  2. 写出用 Tailwind 创建一个宽 300px、高 200px、背景蓝色、圆角、带阴影的 div 的 className。
  3. sm:w-1/2 md:w-1/3 lg:w-1/4 这个 className 的含义是什么?
  4. 如果 Tailwind 默认主题色不满意,应该在哪里修改?请简述步骤。
  5. 为什么 Tailwind 在生产环境的 CSS 体积很小?是什么机制在起作用?

四种方案终极选型对比表

维度 内联样式 普通外部 CSS CSS Modules CSS-in-JS
(styled-components)
Tailwind CSS
学习成本 极低 极低 中高(熟悉工具类后效率极高)
动态样式能力 极强 中(配合 clsx)
样式隔离 天然隔离 无(全局污染) 编译时隔离 运行时隔离 无(但原子类不冲突)
伪类/媒体查询 不支持 支持 支持 支持 支持
运行时性能 最优 最优 最优 一般(运行时开销) 最优
生产体积 0 取决于文件大小 取决于文件大小 约 12KB gzip 约 3-10KB gzip
SSR 友好度 完美 完美 完美 需额外配置 完美
TypeScript 支持 一般 一般 优秀
开发体验 一般 一般 优秀 极好(配合插件)
团队协作 易混乱 需要严格规范 较好 最好(天然约束)

按场景推荐

项目场景 推荐方案 理由
个人学习/练手项目 CSS Modules 上手快,无额外依赖,满足大多数需求
小团队(2-5人)快速迭代 Tailwind CSS 开发效率最高,无命名烦恼
中大型项目(5+人) CSS Modules + Tailwind 混合 CSS Modules 处理复杂组件样式,Tailwind 处理快速布局
设计系统 / 组件库 styled-components 或 CSS Modules 需要强动态样式和主题化能力
Next.js SSR 项目 CSS Modules 或 Tailwind CSS SSR 兼容性最好,零配置
追求极致性能 内联样式 + CSS Modules 减少运行时 JS 计算
传统 CSS 团队转型 React CSS Modules 思维方式最接近传统开发

全文总结

四种 CSS 方案各有适用场景,没有绝对的"最好",只有"最合适":

  1. 内联样式——动态性的极端,适合样式随 JS 变量频繁变化的场景
  2. 普通外部 CSS——最传统的方案,适合页面级全局布局和 CSS 初学者
  3. CSS Modules——在传统 CSS 基础上加了作用域隔离,是 React 官方推荐的"进阶 CSS 写法"
  4. styled-components——样式即组件,适合构建组件库和需要高度动态化的项目
  5. Tailwind CSS——原子化 + 工具类优先,适合追求极致开发效率和团队风格统一的场景

作为教学建议:初学者可以先从 CSS Modules 入手,掌握 React 中 CSS 的基本用法后,再根据项目需求选择学习 styled-components 或 Tailwind CSS。


版权声明:本教程所有代码示例均为教学目的编写,可自由使用、修改和分发。

0

评论区