CSS IN JS(@emotion/react + @emotion/styled)
什么是 CSS IN JS
传统的 React 项目里,样式写在独立的 .css 文件里,再用 className 挂到元素上。但这种方式有几个痛点:
- 类名容易冲突(起名都得想半天,还怕和别人重名)。
- 样式是"死"的,想根据组件状态(比如按钮是否禁用)改样式很麻烦。
- 删组件时常常忘了删对应的 CSS,越堆越多。
CSS IN JS 就是"把样式直接写在 JS/TSX 文件里",样式跟着组件走,不用单独开 .css 文件。它的好处:
- 样式跟着组件走:组件删了,样式也一起没了,不会留垃圾。
- 样式可以根据状态动态变化:直接读
props、读state,像写 JS 一样写样式。 - 不用担心类名冲突:Emotion 会自动生成独一无二的类名。
Emotion 提供了两种写法,正好对应两个包:
| 包 | 用法 | 适合场景 |
|---|---|---|
@emotion/react | 用 css 函数生成样式,挂到 className 上 | 少量、临时的样式 |
@emotion/styled | 创建"自带样式的组件"(Styled Components) | 主要推荐,可复用、可读性好 |
安装
pnpm add @emotion/react @emotion/styled- 找到tsconfig.app.json 添加
"jsxImportSource": "@emotion/react",- 找到vite.config.ts 添加
import { fileURLToPath, URL } from "node:url";
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
// https://vite.dev/config/
export default defineConfig({
plugins: [react({ compiler: true, jsxImportSource: "@emotion/react" })],
resolve: {
alias: {
"@": fileURLToPath(new URL("./src", import.meta.url)),
"~": fileURLToPath(new URL("./src/assets", import.meta.url)),
},
},
});一、@emotion/react:用 css 写样式
1.1 最基础的用法
@emotion/react 导出一个 css 函数,把样式写在模板字符串里,然后挂到元素的 css 属性上:
/** @jsxImportSource @emotion/react */
// 上面这行注释告诉 TS/构建工具:这个文件用 Emotion 处理 css 属性
import { css } from "@emotion/react";
export default function Demo() {
return (
<div
css={css`
padding: 20px;
background: #f0f0f0;
color: #333;
`}
>
这是一个带样式的 div
</div>
);
}注意:文件顶部的 /** @jsxImportSource @emotion/react */ 是让 css 属性生效的关键(Vite/CRA 新版都支持)。如果不写,就需要在 tsconfig.json 或构建工具里全局配置。
1.2 简化写法(直接用对象)
css 除了模板字符串,还支持对象写法(更像写 JS 对象,适合简单样式):
import { css } from "@emotion/react";
const boxStyle = css({
padding: "20px",
background: "#f0f0f0",
color: "#333",
});
export default function Demo() {
return <div css={boxStyle}>带样式的 div</div>;
}1.3 样式复用与组合
css 生成的样式可以当变量复用,还能组合:
import { css } from "@emotion/react";
const base = css({
padding: "10px",
borderRadius: "4px",
});
const primary = css({
background: "#1677ff",
color: "#fff",
});
export default function Demo() {
// 组合多个样式,后面的会覆盖前面的
return <div css={[base, primary]}>组合样式</div>;
}1.4 动态样式(根据 props 变化)
这是 CSS IN JS 最核心的优势:样式可以读组件的 props 动态生成:
import { css } from "@emotion/react";
interface ButtonProps {
disabled?: boolean;
}
export default function Button({ disabled }: ButtonProps) {
return (
<button
css={css({
padding: "8px 16px",
border: "none",
borderRadius: "4px",
// 根据 props 动态改变样式
background: disabled ? "#ccc" : "#1677ff",
color: "#fff",
cursor: disabled ? "not-allowed" : "pointer",
})}
>
按钮
</button>
);
}1.5 写嵌套与伪类
模板字符串写法和普通 CSS 一样,支持嵌套和伪类(&:hover、&:focus):
<div
css={css`
background: #fff;
&:hover {
background: #f0f0f0;
}
.title {
font-size: 20px;
}
`}
>
<div className="title">标题</div>
</div>二、@emotion/styled:创建带样式的组件
2.1 最基础的用法
@emotion/styled 导出一个 styled 对象,可以把任意 HTML 标签变成"自带样式的组件":
import styled from "@emotion/styled";
// 创建一个带样式的 div 组件
const Box = styled.div`
padding: 20px;
background: #f0f0f0;
color: #333;
`;
export default function Demo() {
return <Box>这是一个带样式的盒子</Box>;
}用法和普通组件一模一样,只是自带了样式。这是 Emotion 最推荐的写法。
2.2 根据 props 动态样式
styled 最实用的能力:直接在模板字符串里读 props 改样式:
import styled from "@emotion/styled";
interface ButtonProps {
primary?: boolean;
disabled?: boolean;
}
const Button = styled.button<ButtonProps>`
padding: 8px 16px;
border: none;
border-radius: 4px;
color: #fff;
/* 根据 props 动态返回样式 */
background: ${(props) => (props.primary ? "#1677ff" : "#999")};
cursor: ${(props) => (props.disabled ? "not-allowed" : "pointer")};
opacity: ${(props) => (props.disabled ? 0.6 : 1)};
&:hover {
background: ${(props) => (props.primary ? "#0958d9" : "#888")};
}
`;
export default function Demo() {
return (
<>
<Button primary>主要按钮</Button>
<Button disabled>禁用按钮</Button>
</>
);
}注意:styled.button<ButtonProps> 里的泛型告诉 TS 这个组件接收哪些 props,这样写既能动态改样式,又有类型提示。
2.3 复用已有组件的样式
如果想给一个已经存在的组件(包括自己写的或第三方组件)加样式,用 styled(组件):
import styled from "@emotion/styled";
const MyButton = ({ children, ...props }) => (
<button {...props}>{children}</button>
);
// 给 MyButton 加样式
const StyledMyButton = styled(MyButton)`
background: #1677ff;
color: #fff;
padding: 8px 16px;
`;
// 甚至可以直接给 antd 的组件加样式
import { Button } from "antd";
const StyledAntdButton = styled(Button)`
border-radius: 20px;
`;2.4 对象写法
styled 也支持对象写法,和模板字符串二选一:
import styled from "@emotion/styled";
const Box = styled.div({
padding: "20px",
background: "#f0f0f0",
color: "#333",
});三、两个包怎么选
- 简单、临时的样式:用
@emotion/react的css,快速写几行。 - 需要复用、需要 props 动态变化的组件:用
@emotion/styled,把样式和组件绑定,更清晰。
实际项目里,绝大多数情况推荐用 @emotion/styled,因为它把"样式"和"组件"绑定在一起,代码结构更清晰,也更好复用。
四、完整小案例:一个可复用的卡片组件
把上面学的串起来,做一个"可传标题、可传背景色"的卡片组件:
import styled from "@emotion/styled";
interface CardProps {
bgColor?: string;
}
const CardWrapper = styled.div<CardProps>`
border: 1px solid #e8e8e8;
border-radius: 8px;
padding: 20px;
background: ${(props) => props.bgColor || "#fff"};
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
&:hover {
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
}
`;
const CardTitle = styled.h3`
margin: 0 0 12px;
font-size: 18px;
color: #333;
`;
const CardContent = styled.div`
color: #666;
line-height: 1.6;
`;
interface Props extends CardProps {
title: string;
children: React.ReactNode;
}
export default function Card({ title, children, bgColor }: Props) {
return (
<CardWrapper bgColor={bgColor}>
<CardTitle>{title}</CardTitle>
<CardContent>{children}</CardContent>
</CardWrapper>
);
}使用:
<Card title="默认卡片">内容</Card>
<Card title="高亮卡片" bgColor="#e6f4ff">内容</Card>五、常用技巧速查
| 需求 | 写法 |
|---|---|
| 给标签加样式 | styled.div\...`` |
| 给已有组件加样式 | styled(MyComponent)\...`` |
| 根据 props 改样式 | ${props => ...}` |
| 写 hover 等伪类 | &:hover { ... } |
| 组合多个样式 | css={[styleA, styleB]} |
| 加 TS 类型 | styled.div<Props>\...`` |
| 响应式 | 模板字符串里直接写 @media |
六、小结
@emotion/react提供css,适合写"一次性"样式,灵活。@emotion/styled提供styled,把样式和组件绑定,日常开发首选。- 核心优势:样式跟着组件走、样式可以根据 props 动态变化、不用操心类名冲突。
上手建议:先用 @emotion/styled 写几个小组件(按钮、卡片、标题),熟练后再按需用 css 处理零散样式即可。