CSS-in-JS
CSS-in-JS 是一种把样式纳入组件逻辑系统的方案,通过 JavaScript 管理样式生成、组合、主题和动态状态。
#type / concept
#status / growing
#tech / dev / frontend
#resource / css
#resource / css-in-js
[!info] related notes
- 所属 MOC: CSS MOC
- 前置概念: CSS
- 并列概念: CSS Modules, BEM 命名规范, Sass / SCSS, tailwind
- 易混淆概念:
- 关系笔记: 前端样式方案演进与选型
CSS-in-JS
一句话定义
CSS-in-JS 是一类把样式定义、组合和注入交给 JavaScript 管理的方案,使样式可以直接响应组件的 props、state 和 theme。
核心机制 / 工作原理
CSS-in-JS 不是简单地”把 CSS 写进 JS 字符串”。
它更接近一套样式运行或编译系统,通常会负责:
- 为组件样式生成唯一 className。
- 生成或提取对应的 CSS。
- 把样式注入页面,或在构建期输出静态 CSS。
- 根据 props、state、theme 组合出不同样式。
从工程角度看,CSS-in-JS 让样式从”组件外部资源”变成”组件逻辑系统的一部分”。
代码示例
styled-components(运行时方案)
import styled, { ThemeProvider } from 'styled-components'
// 基础组件
const Button = styled.button`
padding: 8px 16px;
border-radius: 6px;
background: ${props => (props.disabled ? '#ccc' : props.theme.primary)};
color: white;
cursor: ${props => (props.disabled ? 'not-allowed' : 'pointer')};
`
// 继承扩展
const DangerButton = styled(Button)`
background: ${props => (props.disabled ? '#ccc' : '#ff4d4f')};
`
// 使用主题
const theme = { primary: '#1677ff' }
function App() {
return (
<ThemeProvider theme={theme}>
<Button>提交</Button>
<Button disabled>禁用</Button>
<DangerButton>删除</DangerButton>
</ThemeProvider>
)
}
Emotion(运行时方案)
/** @jsxImportSource @emotion/react */
import { css } from '@emotion/react'
import styled from '@emotion/styled'
// css prop 方式
const style = css`
color: hotpink;
font-size: 16px;
`
function Component() {
return <div css={style}>Hello</div>
}
// styled 方式
const Container = styled.div`
display: flex;
gap: ${props => props.gap || 8}px;
`
Vanilla Extract(编译时方案)
// styles.css.ts
import { style } from '@vanilla-extract/css'
export const container = style({
display: 'flex',
padding: 16,
backgroundColor: '#f5f5f5',
})
// 根据变量生成变体
import { recipe } from '@vanilla-extract/recipes'
export const button = recipe({
base: {
padding: '8px 16px',
borderRadius: 6,
},
variants: {
color: {
primary: { background: '#1677ff', color: 'white' },
danger: { background: '#ff4d4f', color: 'white' },
},
size: {
small: { fontSize: 12 },
large: { fontSize: 18 },
},
},
})
编译时方案在构建期生成静态 CSS,运行时零开销。
SSR 考虑
运行时 CSS-in-JS 在服务端渲染时需要特殊处理:
- 样式收集:服务端渲染时收集组件用到的样式,注入到 HTML 的
<style>标签中。 - hydration 一致性:客户端 hydrate 时,必须确保样式与服务端一致,否则会出现样式闪烁(FOUC)。
// styled-components SSR 示例
import { ServerStyleSheet } from 'styled-components'
const sheet = new ServerStyleSheet()
const html = renderToString(sheet.collectStyles(<App />))
const styleTags = sheet.getStyleTags() // 注入到 <head>
- Next.js 集成:需要在
_document中配置样式收集逻辑,否则首屏会没有样式。
编译时方案(Vanilla Extract、Linaria)不涉及运行时注入,SSR 无额外成本。
性能对比
| 方案 | 运行时开销 | SSR 复杂度 | 包体积影响 | 动态样式能力 |
|---|---|---|---|---|
| styled-components / Emotion | 中等 | 高 | 较大 | 强 |
| Vanilla Extract / Linaria | 零 | 低 | 小 | 弱 |
| StyleX(Meta) | 零 | 低 | 极小 | 中等 |
| CSS Modules | 零 | 低 | 小 | 弱 |
运行时方案在频繁切换样式(如动画、大量列表项)时可能有性能瓶颈,因为每次渲染都需要计算样式和插入 <style> 标签。
边界与易混淆点
- CSS-in-JS 的主要优势是动态性和主题系统,不是所有项目都需要这层能力。
- 运行时 CSS-in-JS 会带来一定成本:生成样式、插入
style标签、SSR 样式收集和 hydration 一致性问题。 - 调试时看到的 className 可能不如手写类名直观。
- 它不应该替代浏览器原生 CSS 的所有能力:
@media、CSS variables、container queries、cascade layers 等仍应优先交给 CSS。 - 现代 CSS-in-JS 已经分化成两类:
- 运行时方案,如
styled-components、Emotion - 编译时方案,如
Vanilla Extract、Linaria、StyleX
- 运行时方案,如
- 如果项目更看重首屏性能、SSR 稳定性和运行时开销,往往会更偏向编译时方案、CSS Modules 或原生 CSS 变量。
- React Server Components 的兴起让运行时 CSS-in-JS 方案面临挑战,因为 RSC 中不能使用 useState 等客户端特性,而 styled-components 等依赖客户端运行时。