React(四):React 中 CSS 使用全方位指南
目录
- 环境搭建前置说明
- 第一章:基础内联样式(Inline CSS)
- 第二章:外部 CSS 文件
- 第三章:CSS-in-JS 方案
- 第四章:Tailwind CSS 原子化 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变成backgroundColor,font-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-top → marginTop |
| 像素值可省略单位 | 值为数字时,React 自动追加 px |
width: 200 → width: 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-top→marginTop、border-radius→borderRadius、z-index→zIndex。
坑 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 等) |
| 快速原型开发、演示代码 | 复杂的响应式布局 |
| 动画库中动态修改的样式 | 团队协作的大型项目 |
本章思考练习题
- 下面代码有什么问题?请指出并修正:
<div style="color: red; font-size: 16px;">Hello</div>- 为什么要使用小驼峰命名法(camelCase)?
z-index在 React 内联样式中应该怎么写?- 如何使用内联样式实现一个宽高为
200px的红色正方形,并将其水平居中?- 如果想在点击按钮时切换按钮的背景色,应该使用什么 React 机制配合内联样式实现?
- 内联样式能否实现一个在鼠标悬停时放大 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 类型声明 |
本章思考练习题
- JSX 中为什么用
className而不是class?两者在使用上有什么区别?- CSS Modules 的文件命名有什么特殊要求?如果命名为
styles.css而非styles.module.css,会发生什么?- 在 CSS Modules 中,如果想让某个类名保持全局可见(不被哈希化),应该使用什么语法?
- 请解释 BEM 命名规范中
__和--分别代表什么含义。使用 BEM 规范的根本目的是什么?- 普通外部 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中的服务端样式注入逻辑。
本章思考练习题
- CSS-in-JS 中的 "J" 指的是什么?它解决的核心痛点是什么?
- styled-components 中为什么使用
&符号表示伪类和嵌套选择器?- 请写出一个 styled-components 的代码片段:创建一个
Box组件,接收$bgColorprop 控制背景色,当$bgColor不传时默认为#f0f0f0。- Emotion 相比 styled-components 有什么独特的 API?什么场景下更适合选择 Emotion?
- 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-500、text-white、border-gray-200 |
| 圆角 | rounded- |
rounded-lg(0.5rem)、rounded-full(完全圆形) |
| 阴影 | shadow- |
shadow-md、shadow-xl |
| 字体 | font-, text- |
font-bold、text-lg、text-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:
- 开发阶段:Tailwind 按需生成 CSS,但在开发模式下仍会生成所有工具类以方便调试
- 生产构建:根据
content配置扫描src/下所有文件,只保留实际使用的工具类,未使用的会被"摇掉"(tree-shake) - 最终体积:一个典型的 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中的主题,而非完全使用默认值 - 不适合需要极端像素级还原的特殊设计稿
本章思考练习题
- Tailwind CSS 的"原子化 CSS"是什么意思?它和传统写自定义类的方式有什么本质不同?
- 写出用 Tailwind 创建一个宽 300px、高 200px、背景蓝色、圆角、带阴影的 div 的 className。
sm:w-1/2 md:w-1/3 lg:w-1/4这个 className 的含义是什么?- 如果 Tailwind 默认主题色不满意,应该在哪里修改?请简述步骤。
- 为什么 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 方案各有适用场景,没有绝对的"最好",只有"最合适":
- 内联样式——动态性的极端,适合样式随 JS 变量频繁变化的场景
- 普通外部 CSS——最传统的方案,适合页面级全局布局和 CSS 初学者
- CSS Modules——在传统 CSS 基础上加了作用域隔离,是 React 官方推荐的"进阶 CSS 写法"
- styled-components——样式即组件,适合构建组件库和需要高度动态化的项目
- Tailwind CSS——原子化 + 工具类优先,适合追求极致开发效率和团队风格统一的场景
作为教学建议:初学者可以先从 CSS Modules 入手,掌握 React 中 CSS 的基本用法后,再根据项目需求选择学习 styled-components 或 Tailwind CSS。
版权声明:本教程所有代码示例均为教学目的编写,可自由使用、修改和分发。
评论区