Panda CSS 和 StyleX 都在构建时将样式编译为静态原子化 CSS,且不会向浏览器下发任何样式运行时。两者的区别在于各自要解决的问题:Panda 着眼于强制落实设计系统,而 StyleX 着眼于防止组件之间的样式相互冲突。
当团队希望从 styled-components 或 Emotion 迁移出来,同时又不想放弃类型化、与组件同置(colocated)的样式写法时,这两个库往往会同时进入候选名单,而且两者的 README 读起来几乎如出一辙。本文将阐明两者之间唯一的结构性差异,分别用两个库实现同一个 Button 组件,并梳理这一差异如何影响开发工作流、主题定制和动态值的处理。如需了解更宏观的背景,请参阅 2026 年 CSS-in-JS 现状。
核心要点
- Panda CSS 和 StyleX 都在构建时将样式提取为原子化 CSS,因此两者都不会在浏览器渲染期间注入样式。
- Panda 以
panda.config.ts中的设计令牌(design token)作为唯一可信来源,并据此生成带类型的css()、cva()和styled()辅助函数。 - StyleX 让每个组件通过
stylex.create()定义样式,再由stylex.props()进行合并:对于同一属性,最后传入的样式生效。 - 两个库都无法把仅在运行时才存在的值转换为原子类。StyleX 通过 CSS 变量传递这类值,而 Panda 的文档则建议使用 recipe、CSS 变量或 data 属性。
- Panda 适合设计系统落地不断走样的团队,StyleX 适合样式泄漏屡屡成为顽疾的大型代码库,而许多项目其实两者都不需要。
Panda CSS 与 StyleX 的共同点
Panda CSS 和 StyleX 都是零运行时(zero-runtime)的 CSS-in-JS 库。你用 TypeScript 编写样式对象,由构建步骤将其提取出来,浏览器最终拿到的是一份由单一用途原子类组成的普通样式表。styled-components、Emotion 这类运行时库的工作方式则不同:它们在渲染期间通过 JavaScript 生成并注入样式。而 Panda 和 StyleX 在代码发布前就已完成了全部这些工作。如果你想了解这一分野的早期版本,可以参阅 Linaria vs styled-components。
由于产物是静态 CSS,两个库都不需要 context provider,也不需要渲染时的样式注入器。这正是两者都能与 React Server Components 配合使用的原因:无论是服务端渲染还是客户端渲染,都不会执行任何样式代码。在原子化产物中,每个类只设置一个属性,因此样式表体积随唯一声明的数量增长,而非随组件数量增长。
Panda CSS:设计系统优先
Panda CSS 将设计系统视为唯一可信来源。在 panda.config.ts 中定义的令牌会驱动一个代码生成步骤,为代码库的其余部分生成带类型的 css()、cva() 和 styled() 辅助函数。Panda 出自 Chakra UI 背后的团队,其组件库基因显而易见。
运行 panda codegen 命令会在项目中生成一个 styled-system/ 目录。组件从该目录导入辅助函数,而其中的类型均由你的令牌生成。如果 brand 是一个颜色令牌,那么输入 bg: 'brand' 时就会有自动补全。开启 strictTokens 配置项后,直接写原始的十六进制颜色值会触发类型错误。
Recipe 则把这套机制转化为组件 API。cva() 接收 base 基础样式、具名的 variants 以及 defaultVariants,并返回一个将变体 props 映射为类名的函数。可选项由设计系统决定,组件只能从中挑选。
StyleX:冲突问题优先
StyleX 将样式冲突视为核心问题。每个组件通过 stylex.create() 定义自己的样式对象,再由 stylex.props() 以确定性的方式合并:当两个样式设置了同一属性时,最后传入的那个生效。样式表顺序和选择器优先级在这里都不起作用。StyleX 由 Meta 开源,也是 Astryx 等设计系统底层的样式层。
const styles = stylex.create({
a: { color: 'red' },
b: { color: 'blue' },
})
stylex.props(styles.a, styles.b) // blue
stylex.props(styles.b, styles.a) // red
StyleX 的合并规则同样覆盖简写属性。Thinking in StyleX 中解释道,简写属性与完整属性之间的冲突总是以相同方式解决:在默认配置下,无论传入顺序如何,marginTop 都会覆盖 margin。编译工作由 StyleX 的 Babel 插件完成,官方的打包工具集成和 PostCSS 集成均构建于其上。
用两种方式实现同一个 Button
下面用 Panda CSS 和 StyleX 实现的两个 Button 都对外暴露同一个 intent prop,区别在于样式决策放在哪里。
// Panda: panda.config.ts defines theme.extend.tokens.colors.brand = { value: '#2563eb' }
import { cva } from '../styled-system/css'
const button = cva({
base: { px: '4', py: '2', borderRadius: 'md', fontWeight: 'semibold' },
variants: {
intent: {
primary: { bg: 'brand', color: 'white' },
ghost: { bg: 'transparent', color: 'brand' },
},
},
defaultVariants: { intent: 'primary' },
})
export function Button({ intent, ...props }) {
return <button className={button({ intent })} {...props} />
}
// StyleX
import * as stylex from '@stylexjs/stylex'
const styles = stylex.create({
base: { paddingInline: 16, paddingBlock: 8, borderRadius: 6, fontWeight: 600 },
primary: { backgroundColor: '#2563eb', color: 'white' },
ghost: { backgroundColor: 'transparent', color: '#2563eb' },
})
export function Button({ intent = 'primary', style, ...props }) {
return <button {...props} {...stylex.props(styles.base, styles[intent], style)} />
}
在 Panda 版本中,bg: 'brand' 之所以能通过类型检查,是因为配置中存在 brand,且 codegen 生成了对应的类型。在 StyleX 版本中,每个变体都是独立的对象,使用方传入的 style 排在最后。只要它与基础样式或变体样式设置了相同属性,就以使用方的值为准,完全无需与优先级较劲。Panda 的辅助函数约束的是开发者可以使用哪些值,而 StyleX 的合并规则约束的是一个组件的样式如何覆盖另一个组件的样式。
选择 Panda CSS 或 StyleX 的实际影响
两者出发点的不同,体现在日常开发的三个方面:构建方式、主题机制,以及对仅在运行时存在的值的处理。
| 对比维度 | Panda CSS | StyleX |
|---|---|---|
| 构建步骤 | Codegen 生成 styled-system/;由 PostCSS 插件或 CLI 提取 CSS | Babel 插件编译 stylex.create 调用 |
| 主题定制 | 配置中的令牌 | defineVars 配合 createTheme |
| 运行时值 | 无法提取;改用 recipe、CSS 变量、data 属性 | 通过 CSS 变量传递 |
工作流:代码生成 vs 编译器插件
在 Panda 中,配置与组件之间隔着一层生成产物,因此修改令牌就意味着要重新生成 styled-system/。StyleX 没有生成目录,Babel 插件在每个模块原地完成转换,而导入和变量的解析方式由插件配置控制。
主题:令牌 vs 变量
在 Panda 中,主题就是令牌集合:在配置中修改 brand,下次构建后所有用到它的地方都会随之改变。在 StyleX 中,主题是叠加在组件级样式之上的一种覆盖机制。stylex.defineVars 在 .stylex.ts 文件中声明变量,stylex.createTheme 则生成覆盖值,你需要将其应用到某个祖先元素上:
// tokens.stylex.ts
import * as stylex from '@stylexjs/stylex'
export const colors = stylex.defineVars({ brand: '#2563eb' })
// app.tsx
import * as stylex from '@stylexjs/stylex'
import { colors } from './tokens.stylex'
const dark = stylex.createTheme(colors, { brand: '#60a5fa' })
<div {...stylex.props(dark)}>{children}</div>
StyleX 的主题 API 只有在配置了 Babel 插件的 unstable_moduleResolution 选项后才能正常工作。
动态值:零运行时的软肋
构建时提取器只能看到源码中写明的值。对于仅在运行时才能确定的值(例如用户选择的颜色),两个库都无法将其转换为原子类。Panda 的动态样式指南说明,其提取器会跳过所有无法从源码中读取的内容,并引导你改用 recipe、CSS 变量或 data-* 属性。StyleX 处理动态样式的方式则不同:编译器会生成一个指向 CSS 变量的固定类,运行时的值再被写入该元素上的这个变量。
Panda CSS vs StyleX:你面临的是哪种问题?
应根据你实际面临的问题做选择,而不是比较功能清单。如果是一个小团队,UI 总是偏离自身的设计系统,那正是 Panda 要解决的问题:令牌变成类型,不合规范的值直接无法通过编译。如果是一个大型代码库,某个组件的样式总是泄漏到另一个组件中,那正是 StyleX 要解决的问题:每个组件各自拥有自己的样式,最终结果由调用处的合并顺序决定。
许多项目其实两种问题都没有。对它们而言,CSS Modules 或某个原子化工具框架就足以胜任,所需的构建工具也比这两个库都少。如需了解另外两种样式方案的对比,请参阅 Tailwind vs Sass。
在决定采用其中任何一个库之前,先找出你的团队反复修复的是哪类样式 bug。如果是设计走样,就用 Panda 做一个 recipe 原型;如果是覆盖冲突,就把一个共享组件迁移到 StyleX,观察其使用方的表现。
常见问题
Panda CSS 和 StyleX 在处理 hover 状态和媒体查询上有何不同?
在 StyleX 中,伪类和媒体查询以条件对象的形式写在属性值内部,且该对象必须包含一个 default 条目(可以设为 null)。::placeholder 等伪元素是例外:它们作为独立的键写在命名空间的顶层。Panda 既支持在属性内部写条件,例如 bg: { base: 'red.500', _hover: 'red.700' },也支持条件对象的写法,使用 _hover 等内置条件名以及 md、lg 等移动优先的断点键。
StyleX 能像 Panda 的任意选择器那样为子元素设置样式吗?
不能直接实现。StyleX 不允许使用任意选择器,也不允许“远距离”设置样式,而 Panda 的 css() 支持 '& > *' 这样的任意选择器。StyleX 0.16.0 新增了 stylex.when 系列 API(ancestor、descendant、anySibling、siblingBefore 和 siblingAfter)。你可以用 stylex.defaultMarker() 标记一个元素,当被标记的元素处于 hover、focus 状态或匹配某个属性时,另一个元素的样式随之变化。其中 descendant、anySibling 和 siblingAfter 依赖 CSS 的 :has() 选择器。另一种官方文档中给出的方式,是在父元素上设置 defineVars 变量。
stylex.props() 会在浏览器中运行吗?
有时会,但只是一个轻量的合并步骤。StyleX 编译器在构建时将 stylex.create() 调用转换为静态原子化 CSS;当样式在同一文件中创建和使用时,它可以完全移除 stylex.props() 调用。当样式跨文件使用时,stylex.props() 会作为一个函数运行,从左到右合并已编译的样式对象,并返回供 React 使用的 className 和 style props。它绝不会在浏览器中生成或注入 CSS。对于 Solid、Svelte、Qwik 和 Vue 等非 React 框架,StyleX 还提供了 stylex.attrs()。
Panda CSS 中的 strictTokens 和 strictPropertyValues 有什么区别?
strictTokens 针对的是由令牌支撑的属性,例如 color、bg 和 borderColor:开启后,这些属性只接受令牌值。strictPropertyValues 针对的是一组具有已知关键字取值的固定属性,例如 display、position 和 overflow:开启后,如果取值不属于这些关键字,Panda 会报错。两者都在 panda.config.ts 中开启。无论哪种情况,用方括号包裹原始值(如 [123px])都可以绕过限制,而不会产生 TypeScript 错误。
Gain Debugging Superpowers
Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers.
Star on GitHub12k