时至今日,CSS增强方案已经越来越成熟,从模块化到预处理,从 CSS-IN-JS 到原子化,各个细分领域都已经遍地开花。
这篇文章将从目前比较主流的各类方案说起,将这些领域的代表性解决方案和使用技巧都简单概述一下。希望能给你带来一个较为完整的CSS生态全貌。为团队内部使用CSS增强方案的选项提供一些参考和思路。
CSS Modules
CSS Modules 不是官方规范或浏览器中的实现,而是构建步骤中的一个过程,它通过 module.css
,styles.css
等约定俗成的文件后缀名,将一个文件的类名和选择器作为模块,从而解决全局样式冲突的问题,实现模块化。
css
/* styles.css */
.myButton {
background-color: blue;
}
tsx
import React from 'react';
import styles from './styles.css';
const MyButton = () => (
<button className={styles.myButton}>Click me</button>
);
export default MyButton;
CSS Modules 的工作原理主要基于以下两个步骤:
- 局部化样式 :CSS Modules 会将你定义的 CSS 类名转换为唯一的、只在当前模块有效的类名,从而避免了全局作用域中的样式冲突。例如,上面例子中的
myButton
可能会被转换为styles_myButton_abc123
这样的类名。 - 生成样式对象 :CSS Modules 会生成一个样式对象,这个对象的键是你定义的类名,值是转换后的唯一类名。你可以在 JavaScript 中通过这个对象来访问你的样式。例如,上面例子中的
styles
对象可能会是{ myButton: 'styles_myButton_abc123' }
。
需要注意的是,由于 CSS Modules 是构建步骤中的一个过程,使用 CSS Modules 需要配置你的构建系统(如 Webpack 或 vite)来处理 CSS 文件。在 Webpack 中,你可以使用 css-loader
来启用 CSS Modules。
类型,插件和扩展
由于 CSS Modules 在被 ts
/ vue
/ tsx
等文件引用后,并不会显式地提供类型,可能会被报错。我们可以通过声明一个 .d.ts
文件的方式声明引用类型,或者通过各个构建工具提供的插件来解决。
ts
// type.d.ts
declare module '*.scss';
另外,在实际开发中,经常会遇到根据文件引用知道其引用的对应 CSS 模块的类名,但是不知道类名具体的内容是什么的情况,例如: calssName={style.box}
。这个时候需要我们点进相对应的 CSS 文件才能知道对应的 CSS 值。
我们可以使用 CSS Modules
插件解决上述问题。插件同时可以提供快速跳转和拼写提示的能力。
CSS 增强(预 / 后处理器)
Sass / Less
Sass 和 Less 都是 CSS 预处理器,它们提供了许多 CSS 不具备的功能,如变量、嵌套规则、混入(Mixins)、函数等,从而使 CSS 的编写变得更加高效和强大。
-
Sass:Sass(Syntactically Awesome Stylesheets)是最早的 CSS 预处理器,它提供了两种语法:Sass(缩进语法)和 SCSS(类 CSS 语法)。Sass 支持变量、嵌套规则、混入、函数、继承等功能。此外,Sass 还有一个强大的社区和丰富的插件生态系统。
scss// 变量 $primary-color: #333; .container { background: $primary-color; // 嵌套 .item { color: lighten($primary-color, 20%); } } // 混入 @mixin border-radius($radius) { -webkit-border-radius: $radius; -moz-border-radius: $radius; -ms-border-radius: $radius; border-radius: $radius; } .button { @include border-radius(10px); }
-
Less:Less(Leaner Style Sheets)是另一个流行的 CSS 预处理器,它的语法非常接近 CSS,因此学习曲线较低。Less 同样支持变量、嵌套规则、混入、函数等功能。但是,与 Sass 相比,Less 的功能和社区支持稍微弱一些。
另外,Sass 编译需要使用 node-sass
。他和node版本是一一对应的,不同的node版本需要不同的 node-sass
,这一点在团队开发时需要格外注意。(可以使用 dart sass
代替)
总结来说,Sass 使用人数多,生态广,具有插件系统,功能更多;Less 相对来说学习曲线平缓,更接近 CSS。
PostCSS
PostCSS 是一个用 JavaScript 工具和插件转换 CSS 代码的工具。使用 Post-CSS 的插件,可以增强 CSS 的各种特性。
插件系统
- Autoprefixer:使用 Autoprefixer,PostCSS 会根据不同厂商添加不同的规则,来做到 CSS 规则在不同的浏览器中的适配。
- CSS 模块 :使用 PostCSS 实现 CSS Modules
- stylelint:样式约束和报错提示
- postcss-scss:将 sass 语法转换为 css
- postcss-import: 支持 import 语法来达到缩小文件体积的作用
- 自定义插件
api
使用 PostCSS 提供的 api 可以对 CSS 进行解析和转换。
-
Declaration CSS 声明
jsconst postcss = require('postcss'); const css = ` .foo { color: red; } `; const root = postcss.parse(css); // 遍历 CSS 声明 root.walkDecls(decl => { console.log(decl.prop); // 输出 "color" console.log(decl.value); // 输出 "red" if (decl.value === 'red') { decl.value = 'blue'; } console.log(decl.toString()); // 输出 "color: blue" }); // 向根节点插入一个CSS声明 const color = new Declaration({ prop: 'color', value: 'black' }) root.append(color)
-
Rule CSS 中的规则
jsconst postcss = require('postcss'); const css = ` .foo { color: red; } `; const root = postcss.parse(css); // 遍历CSS规则 root.walkRules(rule => { console.log(rule.selector); // 输出 ".foo" rule.append({ prop: 'background-color', value: 'white' }); console.log(rule.toString()); // 输出 ".foo { color: red; background-color: white }" }); // 向声明中插入规则 const decl1 = new Declaration({ prop: 'color', value: 'black' }) const decl2 = new Declaration({ prop: 'background-color', value: 'white' }) rule.append(decl1, decl2) root.append({ name: 'charset', params: '"UTF-8"' }) // at-rule root.append({ selector: 'a' }) // rule rule.append({ prop: 'color', value: 'black' }) // declaration rule.append({ text: 'Comment' }) // comment root.append('a {}') root.first.append('color: black; z-index: 1')
Tailwind CSS
Tailwind CSS 是一个高度可定制的、实用程序 优先的 CSS 框架,它提供了大量的低级 CSS 类,这些类可以被组合在一起,以构建任何设计。
提到 Tailwind CSS 就不得不鞭尸一下 Bootstrap 😂,大家在没用 Tailwind 前的第一感受可能会是上手难度高,不灵活,这些印象或多或少是由于 Bootstrap 曾经是这样的。
实用程序优先意味着,而不是预先定义如何在特定元素上实现某种样式(例如 Bootstrap 的 .btn
或 .card
类),Tailwind 允许你使用小的、可复用的类(例如 flex
、font-bold
、mt-4
等)来直接在元素上应用样式。
例如我们想写 display: flex
,那么使用 Tailwind ,我们可以直接在 class 中写 flex
。写 border-color: black
或者 border-color: #123123
我们可以直接写:border-black
和 border-[#123213]
。
IDE增强
上述写法可能还会有上手难度?说实话,有时候我写 CSS 也会突然忘记一些属性或属性值,都是依靠这样式提示才得以流畅开发。对于 tailwind 而言,我们也有对应的 IDE 扩展来实现这种功能(Tailwind CSS IntelliSense),从而极大提升开发体验。

PostCSS插件
Tailwind CSS 实际上是一个 PostCSS 插件,它使用 PostCSS 来生成最终的 CSS 文件。通过配置 PostCSS,你可以利用许多强大的功能,例如:
- 自动添加浏览器前缀:PostCSS 可以自动为你的 CSS 添加浏览器前缀,以确保你的 CSS 在所有浏览器中都能正常工作。
- 自定义 Tailwind CSS:你可以在 PostCSS 配置文件中自定义 Tailwind 的主题,例如颜色、间距、字体等。
- 使用未来的 CSS 特性:通过 PostCSS 插件,你可以在当前的 CSS 中使用未来的 CSS 特性,例如 CSS 嵌套、自定义属性等。
- 优化 CSS:PostCSS 还可以帮助你优化 CSS,例如删除未使用的 CSS、压缩 CSS 等。
prettier
使用 prettier 插件,格式化操作可以按照官方推荐的顺序对类名进行排序。点击查看
CSS-IN-JS
相对于 CSS Modules 和预处理等方案的稳定和较为统一,CSS-IN-JS 方案则属于是百家齐放。CSS-in-JS 是一种将 CSS 样式直接写入 JavaScript 代码的方法。CSS-in-JS 的主要目标是解决 CSS 的全局作用域问题,提高样式的模块化和可复用性,同时和 js 结合在一起,使得样式动态修改更加灵活。
以下介绍了几种比较常见或具有特点的CSS-IN---JS解决方案及其使用方式和特色。注意,下面对每一种框架的介绍仅是简述,一些特色也是我觉得有趣或有用才列举出来。类似变量,主题,全局样式等每个框架都会去实现的功能,这里就不再赘述了。具体文档可以点击标题链接查看。同时也会附上较为主观的总结和评分,仅供参考。
emotion / styled-components
emotion / styled-components 都是基于组件化时代下诞生的具有代表性的 CSS-in-JS 解决方案。其中心思想是将 CSS 样式作为可视化原语构建样式组件。
常见用法:
jsx
// Button可以直接作为一个组件来使用
const Box = styled.div`
color: grey;
.son {
color: red;
}
`;
CSS Api
利用 css
api,我们可以编写一个样式块并用在样式组件(emotion / styled-components)或普通标签上(emotion)
jsx
import { css } from '@emotion/css'
const color = 'darkgreen'
render(
<div
className={css`
background-color: hotpink;
&:hover {
color: ${color};
}
`}
>
This has a hotpink background.
</div>
)
props:
使用props的方式,我们可以根据组件传入的 Props 来动态变更样式。
jsx
onst Button = styled.button<{ primary?: boolean; }>`
/* Adapt the colors based on primary prop */
background: ${props => props.primary ? "#BF4F74" : "white"};
color: ${props => props.primary ? "white" : "#BF4F74"};
${props => props.primary && css`
order: 2px solid aqua;
`}
}
`
export default function() {
return <Button primary>按钮</Button>
}
IDE增强
安装 vscode-styled-components
组件后,可以让相关语法得到高亮和提示增强。

总结&评分(vs CSS Modules)
- 样式组件是一个很经典的 CSS-IN-JS 方案,最大的进步就是不用再写 css 文件。😋
- 内部支持嵌套等 CSS 增强语法。😋
- 与一些类似vanilla-extract 对象语法方案相比,没有对样式类型的检查和运行时优化(emotion有对运行时性能进行优化,但是代价是css体积增加)。🙁
- 对 SSR 支持 styled-component 有一套较为完善的方案,但是 emotion 还正在完善(主要在服务端组件上)。😐
styled-components 使用体验(主观感受):⭐️⭐️⭐️⭐️ 性能 : ⭐️⭐️⭐️ 配套生态:⭐️⭐️⭐️⭐️⭐️
emotion 使用体验(主观感受):⭐️⭐️⭐️⭐️⭐️ 性能 : ⭐️⭐️⭐️⭐️ 配套生态:⭐️⭐️⭐️
vanilla-extract
Vanilla-extract 是一个零运行时的,类型安全的 CSS 样式解决方案。样式文件通过以 css.ts
作为后缀命名,编译时生成对应的 CSS 文件。其生成的 CSS 类名是唯一的,这可以防止样式冲突。
常见用法:
tsx
// a.css.ts
import { styleVariants } from '@vanilla-extract/css';
export const background = styleVariants({
primary: { background: 'blue' },
secondary: { background: 'aqua' }
});
/* 解析成: .styles_background_primary__1hiof570 {
background: blue;
}
.styles_background_secondary__1hiof571 {
background: aqua;
} */
// Section.tsx
interface SectionProps {
variant: keyof typeof background;
}
const Section = ({ variant }: SectionProps) => (
<section className={background[variant]}>...</section>
);
Composition
Vanilla-extract 提供了一种复用方式可以让一些基础样式传递给其他样式,但仍将其视为一个单独的类名。
tsx
import { style } from '@vanilla-extract/css';
const base = style({ padding: 12 });
const primary = style([base, { background: 'blue' }]);
const secondary = style([base, { background: 'aqua' }]);
/* .styles_base__1hiof570 {
padding: 12px;
}
.styles_primary__1hiof571 {
background: blue;
}
.styles_secondary__1hiof572 {
background: aqua;
} */
灵活的选择器系统
使用 selectors
可以实现复杂且灵活的选择器命名:
jsx
import { style } from '@vanilla-extract/css';
const link = style({
selectors: {
'&:hover:not(:active)': {
border: '2px solid aquamarine'
},
'nav li > &': {
textDecoration: 'underline'
}
}
});
// 选择器还可以引用其他作用域的类名。
export const parent = style({});
export const child = style({
selectors: {
[`${parent}:focus &`]: {
background: '#fafafa'
}
}
});
vanilla-extract作为第一个具有代表性的将零运行时和类型安全结合在一起的 CSS-IN-JS 解决方案,在最开始推出的时候备受关注,在 2021年的 CSS-IN-JS 解决方案调查中首次出现就位列榜首(满意度)。然而到了2022年,在大家使用了一年之后,其问题也逐渐暴露出来。满意度也跌至了79%。

总结&评分(vs emotion)
- 零运行时,类型安全和样式隔离让其安全性和性能比 emotion 等框架更加优秀。😋
- Composition,selectors key 和 createVar 等功能让其保持了高灵活和可复用性。😋
- 会有一定的学习成本,同时相对于 emotion 在哪都可以写来说,vanilla-extract 还是比较像 CSS Module,需要特殊的文件名独立起来(css.ts)。🙁
- 由于
vanilla-extract
在构建时将样式编译为 CSS 文件,所以它不支持一些需要运行时动态生成样式的 CSS-in-JS 功能,如依赖于组件状态或 props 的样式,不过非要让编译时生成的CSS去做运行时的工作确实有点吹毛求疵了。😐
使用体验(主观感受):⭐️⭐️⭐️⭐️ 性能 : ⭐️⭐️⭐️⭐️⭐️ 配套生态:⭐️⭐️⭐️
styled-jsx
styled-jsx
是 nextjs 框架自带的 CSS-in-JS 解决方案,我们可以在组件中使用style标签进行scoped样式的编写。
常见用法:
在一个组件中,使用一个 <styled jsx>
,就可以渲染为 css 了。(next.js 已经内置了相关的babel)
jsx
export default () => (
const [color, setColor] = useSate('red')
<div>
<p>only this paragraph will get the style :)</p>
<style jsx>{`
p {
color: ${color};
}
`}</style>
</div>
)
配置了 styled-jsx-plugin-sass
插件后,也可以写 sass 语法。(注意配置sass插件需要统一node版本)
:global() :
我们想让 Wedget 组件在 Home 组件下时才展现出一些效果:使 Wedget 组件下的带有 .btn 类名的元素样式改变,那么我们可以使用 :global() :
jsx
import Widget from '../components/Widget';
function Home() {
return (
<div className="container">
<h1>Hello Next.js</h1>
<Widget />
<style jsx>{`
.container {
margin: 50px;
}
.container :global(.btn) {
background: #000;
color: #fff;
}
`}</style>
</div>
);
}
export default Home;
当然,我们也可以采用预设一些props,向Widget组件内部传入props的形式改变样式。两种方式使用哪一个取决于样式是否相对固定,有一定规律。 例如如果一个 button 大概有 warn,error,info,success四种样式系统,那我们就可以预置在props上,类似于 <MyButton type="warn">
。但如果样式可能被修改的形式比较自由,就可以使用 :global()。在实际使用中,两种往往是结合起来使用,并不冲突。
IDE增强
安装 styled-jsx Syntax Highlighting
和 styled-jsx Language Server
可以让 styled-jsx 相关语法得到高亮和提示增强。

但是由于所有的样式都写在了一个模板字符串内,缺少了可以收缩和打开代码块的效果。
总结&评分(vs emotion & vanilla-extract)
- styled-jsx 提供了一种比较新颖的样式书写形式,和 Vue style 有点异曲同工之妙。由于 CSS 和 HTML 部分并没有以组件或嵌入的方式结合,还是会出现 HTML 结构和 CSS 分离需要来回找,两处编写的情况。😐
- 将同一个组件的样式尽量都限制在了组件内,维护起来还算轻松。😋
- 由于是 nextjs 官方出品,对 SSR 支持较为完善。😋
- 原生只支持 CSS 语法,但其具有 CSS 增强插件系统(虽然有一些老不更新有问题)。😋
- IDE 扩展不完善,开发体验不如 emotion / styled-components。🙁
- 与一些类似vanilla-extract 对象语法方案相比,没有对样式类型的检查和运行时优化。🙁
使用体验(配套插件后的主观感受):⭐️⭐️⭐️ 性能 : ⭐️⭐️ ⭐️ 配套生态:⭐️⭐️⭐️⭐️
其他
这里还有很多 CSS-IN-JS 方案提供了一些更加优秀的性能 / 编码体验:
- Linaria:在 JS 中编写 CSS(写法类似于emotion),但运行时间为零,在构建过程中将 CSS 提取到 CSS 文件中。
- pandacss:将原子化样式和 CSS-IN-JS 融合在一起,并且零运行时,有语法检查和提示。
- kuma-ui: 提供常见的样式组件(例如
Flex
,Link
),和对应的具有原子化特征的 proops。同事提供了在 JS 中编写 CSS(写法类似于emotion)。 - tss-react: 为 next.js 和 jsx 语法定制的零运行时对象形式的 CSS-IN-JS 方案。
但是后起之秀总是会面临生态适配和学习成本的问题。例如 linaria ,至今没有适配SSR;kuma-ui 需要学习其特殊组件的 props。如果没有很新的想法(例如vanilla-extract)或者持续高度活跃的社区反馈,很难出圈让开发者们熟知。
总结至此,这里推荐一个选择方案的优先级选择方式:
- 个人项目 / 小业务:一眼看上去喜欢 >> 开发体验 > 维护性 > 性能 > 生态 > 团队技术栈 > 学习成本
- 长期项目:生态 = 维护性 > 开发体验 > 学习成本 > 性能 > 团队技术栈
- 时间紧急的项目:生态 > 学习成本 > 团队技术栈 > 开发体验 > 维护性 > 性能
- 首页项目,重性能的项目:不用CSS-IN-JS > 性能 > 生态 > 团队技术栈 > 维护性 > 学习成本 > 开发体验
最后,CSS方案只要不是太逆天,其实都可以接受。选择一个CSS框架的本心是为了更好的适配现代前端框架和团队的开发理念。保守的技术选型可以降低开发风险,优秀的生态可以持续增强开发体验。在这些基础之上,才应该再去考虑是否添一些新东西来增强某一方面的开发体验或效果。
我们的新项目,希望可以将 .css
文件淘汰掉,同时性能不能太差,万物皆组件的情绪没有那么强烈,希望可以使用到一些增强的 CSS 语法。最终选择了使用:Tailwind + PostCSS cover 所有 CSS 样式,同时使用 emotion 写一些可以复用的单纯的样式组件。也希望能给你的团队新项目的 CSS 方案选型提供一些灵感。