Menu

React CSS 样式教程:className、内联样式、CSS Modules 与 Tailwind

给 React 组件加样式有几种方式:用 className 配合普通 CSS,用 style prop 写在 JavaScript 中计算出的值,用 CSS Modules 获得作用域隔离的类名,或者用 Tailwind 的工具类。逐一了解每种方式、条件类名,以及各自适用的场景。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

React 组件用普通 CSS 来加样式。用 className prop 给元素设置类名,把规则写在样式表里;值来自 JavaScript 时用 style prop;项目变大后再考虑 CSS Modules 或 Tailwind。下面的示例把 CSS 写在一个 <style> 元素里,这样它才能在本页运行;在实际项目中,这些 CSS 会放在一个 .css 文件里。

把 .btn 规则里的 #2563eb 改成 #16a34a,按钮就会变成绿色。你掌握的所有 CSS 知识在这里都适用:选择器、媒体查询、悬停状态、动画。

用 className,而不是 class

JSX 会被编译成 JavaScript,而 class 在 JavaScript 里是保留字,所以 React 使用 className,也就是 DOM 自己给这个属性起的名字(element.className)。浏览器收到的仍然是一个普通的 class 属性。label 上的 for 也同样改名为 htmlFor。React 19 在开发环境中接受 class,但会给出警告;className 才是正确的名字,也是所有代码库使用的写法。

className 接受一个字符串,所以多个类名用空格分隔:className="btn btn-primary"。因为这个值是 JavaScript,所以它可以通过计算得到。

条件类名

想只在某些情况下添加一个类,就用三元表达式或模板字符串拼出字符串。高亮当前选中标签的标签栏就是典型的例子。

点击 Lessons,下划线就会移过去,因为现在只有这个按钮的字符串里包含 active。Reviews 拿到了 disabled 类和 disabled 属性,所以它是灰色的,点击也没有反应。条件为假时,模板字符串会留下多余的空格,浏览器会忽略它们。当一个组件有很多条件类名时,用数组会更整洁:

const classes = ['tab', isActive && 'active', isDisabled && 'disabled']
    .filter(Boolean)
    .join(' ');

很多项目会用小巧的 clsx 包来做同样的事:clsx('tab', { active: isActive, disabled: isDisabled })。

style prop

style prop 接受的是一个 JavaScript 对象,而不是字符串。这就是它有两层花括号的原因:外层开启一个 JavaScript 表达式,内层是对象字面量。属性名使用驼峰写法(backgroundColor、fontSize、borderRadius),值是字符串或数字。长度类属性上的数字会被当作像素,所以 padding: 12 的意思是 12px。

当某个值要在渲染时计算出来,比如进度条的宽度,内联样式就派上了用场。

一直点击 Add 10%,直到进度条填满,它就会变成绿色。注意 width: percent + '%':单独一个数字表示像素,所以 px 以外的单位要写在字符串里。没有单位的属性则保持纯数字,比如 opacity: 0.8、zIndex: 2、flexGrow: 1 和 lineHeight: 1.5(它是倍数,不是像素)。

内联样式也有局限。它无法表达 :hover、:focus、媒体查询或关键帧动画,而且每个元素都带着一份自己的规则。把它用在动态的值上,其余部分留在 CSS 里。

用 props 设置 CSS 变量

一个不错的折中办法,是在 style 中设置一个 CSS 自定义属性,再在样式表里使用它。规则(包括悬停状态)依然留在 CSS 中,只有这个值来自 JavaScript。

选一个新颜色,两个按钮都会随之改变,而悬停时的填充依然有效,因为那是普通的 CSS。自定义属性名在 style 对象里保留短横线,所以要加引号:{ '--accent': accent }。

CSS 文件

在实际项目中,你会把样式写在 .css 文件里,并从需要它的组件中导入。在 Vite 中,只写导入就够了:构建工具看到 import './Button.css',会在开发时把样式表加到页面上,并在生产环境中把它打包进一个 CSS 文件。这里没有任何导出可用;这个导入只是为了它的副作用。

// Button.jsx
import './Button.css';

export default function Button({ children }) {
    return <button className="button">{children}</button>;
}
/* Button.css */
.button {
    background: #2563eb;
    color: white;
    border-radius: 6px;
}

问题在于,每个被导入的样式表都是全局的。Button.css 里的 .button 规则会作用于应用中任何地方带这个类名的元素,两个文件如果都定义了 .title,就会互相覆盖。团队通常用 BEM(.card__title)这类命名约定,或者用 CSS Modules 来解决这个问题。

CSS Modules

以 .module.css 结尾的文件就是一个 CSS Module。导入它会得到一个对象,把你写的每个类名映射为一个唯一生成的名字,因此一个类只会在你使用它的地方生效。Vite 和 Next.js 都无需任何配置就支持它。

// Card.jsx
import styles from './Card.module.css';

export default function Card({ title, children, featured }) {
    return (
        <section className={featured ? `${styles.card} ${styles.featured}` : styles.card}>
            <h3 className={styles.title}>{title}</h3>
            {children}
        </section>
    );
}
/* Card.module.css */
.card { border: 1px solid #ddd; border-radius: 10px; padding: 16px; }
.featured { border-color: #2563eb; }
.title { margin: 0; }

在浏览器中,这个类名会变成类似 _card_1x2ab_1 的样子。另一个组件可以定义自己的 .title,两者永远不会冲突。你写的是普通 CSS,悬停规则和媒体查询都可以用,这也是 CSS Modules 常被用作组件样式默认方案的原因。

在 React 中使用 Tailwind CSS

Tailwind 采用了另一种思路:你不写规则,而是直接在 className 中组合小巧的工具类。px-4 设置水平内边距,rounded-lg 让边角变圆,hover:bg-blue-700 在悬停时改变背景色。Tailwind 会扫描你的文件,只为你用到的类生成 CSS。

在 Vite 的 React 应用中配置它(Tailwind v4)只需三步:

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

export default defineConfig({
    plugins: [react(), tailwindcss()],
});
/* src/index.css, imported from main.jsx */
@import "tailwindcss";

然后用类名给组件加样式:

function UpgradeCard({ active }) {
    return (
        <div className="max-w-xs rounded-xl border border-gray-200 p-4">
            <h3 className="mb-2 text-lg font-semibold">Pro plan</h3>
            <button
                className={`rounded-md px-4 py-2 text-white ${active ? 'bg-green-600' : 'bg-blue-600 hover:bg-blue-700'}`}
            >
                {active ? 'Active' : 'Upgrade'}
            </button>
        </div>
    );
}

本页的编辑器里不支持 Tailwind,所以这里以静态代码展示。条件类名的写法和上面的标签示例完全一样,但每个类名都要写完整:Tailwind 通过扫描源代码文本来查找类名,所以像 `bg-${color}-600` 这样在运行时拼出来的名字永远不会被生成。

各种方式的适用场景

  • 普通 CSS 文件适合小型应用和全局样式(重置样式、字体、页面布局)。应用变大后要留意类名冲突。
  • CSS Modules 适合大多数组件样式:普通 CSS,自动隔离作用域,无需额外的库。
  • Tailwind 适合喜欢在标记里写样式、并希望间距和颜色有一套统一设计尺度的团队。
  • style prop 用于渲染时计算出的值:宽度、位置、用户选择的颜色。搭配 CSS 变量使用,可以把悬停和媒体查询规则留在 CSS 中。

这些方式可以混用。一个典型的应用会有一个全局样式表,组件用 CSS Modules 或 Tailwind,再加少量内联样式处理动态的值。styled-components 和 Emotion 这类 CSS-in-JS 库是另一种选择,你会在较老的代码库中遇到它们。安装 React 页面介绍了如何创建一个以上方式都能使用的 Vite 项目。

常见问题

React 为什么用 className 而不是 class?

JSX 会被编译成 JavaScript,而 class 在 JavaScript 里是保留字。React 使用的是 DOM 属性名 className,在普通 JavaScript 中 element.className 也叫这个名字。它最终仍会变成 HTML 里一个普通的 class 属性。

React 中怎么写内联样式?

给 style prop 传一个对象,属性名用驼峰写法:style={{ backgroundColor: 'teal', fontSize: 18 }}。外层花括号开启一个 JavaScript 表达式,内层花括号就是这个对象。对于接受长度的属性,数字会被当作像素。

React 中怎样有条件地添加 class?

在 JavaScript 里拼出字符串:className={isActive ? 'tab active' : 'tab'},或者用模板字符串,比如 className={`tab ${isActive ? 'active' : ''}`}。条件很多时,用 clsx 这样的小工具包能让代码保持可读。

React 中的 CSS 文件是全局的吗?

是的。import './App.css' 会把样式表加到整个页面上,所以其中的类名可能和其他文件冲突。CSS Modules(App.module.css)会把每个类重命名为唯一的名字,让它只在你导入的地方生效。

如何在 React 中使用 Tailwind?

在 Vite 项目中安装 tailwindcss 和 @tailwindcss/vite,把插件加到 vite.config.js,再在主 CSS 文件顶部写上 @import "tailwindcss";。之后就可以在 className 中写工具类了,比如 className="px-4 py-2 rounded bg-blue-600 text-white"。

Coddy 编程语言插图

和 Coddy 一起学编程

开始