关于 panda-css-theming
这是面向 Panda CSS 主题与设计系统架构的 skill,帮助团队规划 token 层级、主题、复合样式及 recipe 的职责与命名。它通过检查现有配置和调用点,指导何时将局部样式提升为共享设计系统 API。
主要功能
- 01规划原始与语义 token 层级
- 02设计主题和明暗模式
- 03区分复合样式、recipe 与 slot recipe
- 04制定 token 和变体命名规则
- 05检查已解析的设计系统规格
- 06判断样式何时提升为 recipe
适用场景
- 01为 Panda CSS 项目建立 token 体系
- 02统一主题与明暗模式处理
- 03将重复组件样式整理为 recipe
- 04审查现有设计系统的 token 来源
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ chakra-ui/ panda --skill panda-css-theming安装后在 Claude Code 中输入/panda-css-theming确认已加载。
详细介绍
Panda CSS 主题设计与系统设计
这是架构层:一个令牌叫什么、属于哪一层,以及样式何时应从调用点升级为配方。这些选择一旦代码库中有了数百个调用点,就很难更改。
两个问题就能解答大部分疑问:
- 这个值有含义,还是只是一个值? 有含义的值放入
semanticTokens。 - 这个文件之外是否有人需要改变它? 如果需要,它就是配方;如果不需要,就用
css()。
术语
请准确使用这些词。Panda 的配置键以它们命名,因此这里用词不严谨,之后就会变成错误的配置键。每个条目都说明了应避免的词以及原因。
原始令牌:theme.tokens 下的原始刻度值,按其内容命名(blue.500、spacing.4)。避免使用:“变量”(Panda 会根据令牌生成 CSS 变量,两者并不相同),“基础令牌”(看起来像 { base: … },而它是一个条件)。
语义令牌:theme.semanticTokens 下的值,按其作用命名(fg.muted、danger),并且可以随条件变化。避免使用:“别名”(别名意味着一个固定目标,而语义令牌可以在不同条件下取不同的值),“主题变量”。
复合样式:textStyles、layerStyles 或 animationStyles 下的一组具名属性。避免使用:“预设”(在 Panda 中,预设是可共享的配置包,完全是另一回事),“混入”。
配方:组件的样式及其变体,可以来自文件中的 cva,也可以来自配置中的 theme.recipes。避免使用:“变体函数”,“样式对象”(样式对象是传给 css() 的普通参数)。
插槽:插槽配方中一个具名且独立设置样式的部分。避免使用:“部件”、“元素”、“子组件”。配置键是 slots,使用其他词就会偏离主题。
解剖结构:组件公开的所有插槽的完整列表。这是公共 API。避免使用:“结构”、“形状”。
变体轴:variants 下的一个键(size、visual),其中包含该轴上的选项。避免使用:“属性”(属性是使用者传入的,轴是你定义的),“修饰符”。
复合变体:当特定的轴组合匹配时触发的规则。避免使用:“组合变体”,“多重变体”。
模式:可复用的布局函数(stack、grid、container)。_避免_将模式称为组件,也避免将配方称为模式。Panda 使用这两个词分别表示不同概念,也对应不同的配置键。
条件:状态或媒体选择器键(_hover、_dark、md)。避免使用:“修饰符”,“伪类”(条件也涵盖媒体查询和选择器)。
第 1 步:了解现有内容
panda.config.ts:presets、theme、semanticTokens、conditions、strictTokens、designSystem。<outdir>/types/tokens.d.ts:当前所有可解析的令牌名称。<outdir>/specs/design-system.json:包含相同令牌及其值、条件、配方和各自来源预设的信息。参见设计系统规范。<outdir>/recipes/:现有配方。- 搜索你准备令牌化的属性在若干调用点中的用法。如果已有 8 个文件都将
gray.600用作次要文本颜色,那缺少的就是这个语义令牌。
不要脱离现有内容凭空设计。预设(@pandacss/preset-base、@pandacss/preset-panda)已经提供了一些刻度。应在其基础上扩展,而不是替换它们。
Token 层
四个层,每个层各有不同的职责。将值放在仍能准确描述它的最高层。
1. 基础 token 位于 theme.tokens。不带用途判断的原始刻度值。
tokens: {
colors: { blue: { 500: { value: '#3b82f6' } } },
spacing: { 4: { value: '1rem' } },
}
规则:提供完整刻度,而不只是今天需要的三个色阶。根据它_是什么_来命名(blue.500),绝不根据它的用途命名。
基础 token 的名称中不应提及任何组件。
2. 语义 token 位于 theme.semanticTokens。描述值_代表什么_,以及它如何随条件变化。
semanticTokens: {
colors: {
fg: { value: { base: '{colors.gray.900}', _dark: '{colors.gray.50}' } },
'fg.muted': { value: { base: '{colors.gray.600}', _dark: '{colors.gray.400}' } },
danger: { value: '{colors.red.600}' },
},
}
规则:
- 处理浅色/深色模式的唯一合理位置就在这里。将两个值配对放在同一个 token 中;不要在调用处添加
_dark分支, 而应在这里一次性处理。 - 按角色命名:
fg、fg.muted、bg.subtle、border、danger。不要用gray600Text,也不要用buttonBlue。 - 仅由一个组件使用的 token 是配方值,而不是语义 token。等出现第二个使用者时再提升它,不要提前。
- 使用
{colors.gray.900}引用基础 token。不要将十六进制值直接写入语义 token。
3. 复合样式是 textStyles、layerStyles 和 animationStyles。将经常一起使用的一组属性命名并打包。
textStyles: {
heading: {
h1: { value: { fontSize: '2.5rem', fontWeight: 'bold', lineHeight: '1.2' } },
},
}
当相同的三到五个属性总是一起出现,并且团队已经会用一个名称来称呼它们时,就使用复合样式。 如果它需要变体,那它就是配方。
4. 配方是带有变体的组件样式。见下文。
命名
- 优先按角色而不是外观命名。
danger在品牌重塑后仍然适用;red则不然。 - 使用点号表示层级:
fg、fg.muted、fg.subtle。Panda 会将它们扁平化到同一个命名空间,因此前缀就是分组。 - 选择一个维度并始终遵循。在同一个主题中混用
bg.subtle和subtleBackground的代价比采用其中任一种都高。 - 留意关键字冲突。在
lineHeights中名为normal的 token 会覆盖 CSS 关键字。请重命名该 token。
主题和颜色模式
语义 token 包含两种值。仍然需要有机制来决定应用哪一个。
颜色模式
使用基础预设时,_dark 表示“位于带有 dark 类的元素内”。切换该类后,所有语义 token 都会切换,组件无需知道发生了变化。
<html class="dark"></html>
若要同时匹配数据属性,请重新定义条件。这属于配置更改,因此完成后要运行 codegen。
conditions: {
extend: {
dark: '.dark &, [data-theme="dark"] &',
},
}
不要使用类似 useColorModeValue 的钩子,也不要在组件中添加分支。条件本身就是实现机制。
多个主题
themes 通过一个属性承载多个备选 token 值。只覆盖与默认值不同的部分,并确保每个被覆盖的值都包含两种模式。
themes: {
gothic: {
semanticTokens: {
colors: {
canvas: { value: { base: '{colors.gray.100}', _dark: '{colors.black}' } },
accent: { value: { base: '{colors.purple.700}', _dark: '{colors.purple.300}' } },
},
radii: { control: { value: '{radii.none}' } },
},
},
}
<html data-panda-theme="gothic"></html>
主题和模式是两个独立维度。data-panda-theme 用于选择主题,dark 类用于选择主题内的模式,两者可以叠加。主题会应用于带有该属性的元素及其所有子元素,因此可以在 <html> 上设置一个主题,并在其中的面板上设置另一个主题。
发布主题
除非指定发布方式,否则 Panda 不会为 themes 生成任何内容。有两个选项,彼此独立。
在主样式表中。 在 staticCss.themes 中列出主题。每个主题都会在 tokens 层中生成一个变量块,其中只包含它覆盖的内容。无需运行时开销,只需一次请求,但每个页面都要加载所有主题。
staticCss: {
themes: ['matcha', 'gothic'],
}
按需加载。 <outdir>/themes 会为每个主题提供单独的 JSON 文件,按需获取。主题很多,或用户会在运行时选择主题时,选择此方式。
import { getTheme, injectTheme } from '../styled-system/themes'
const theme = await getTheme('gothic')
injectTheme(document.documentElement, theme)
injectTheme 会在元素上设置 data-panda-theme,并追加一个包含该主题变量的 <style>。
如何选择
- 只有一种外观,包含浅色和深色模式:使用带
_dark的语义 token。不需要themes块。 - 少量固定品牌:使用
themes和staticCss.themes。 - 主题很多,或由用户在运行时选择:使用
themes和getTheme/injectTheme。
如果一个主题覆盖了大部分 token 集合,通常说明语义层不够完善。主题应该覆盖角色,而不是重建刻度。
设计系统规范
此规范为可选项。将 --spec 传给 panda codegen、panda build 或 panda lib,它会写入
<outdir>/specs/design-system.json。--spec=<file> 可将文件写到其他位置。不带此标志时不会写入任何文件,而且
panda dev 不接受此标志。
它将完整解析后的系统放在一个文件中,无需服务端,也不涉及 MCP:
schemaVersion,以及将每个令牌类别映射到paths对应部分的categoriespaths和tokens,提供每个令牌的类别和cssVarvalues,将每个令牌与其解析后的值配对(animations.spin的值为spin 1s linear infinite)conditions,提供每个名称对应的实际选择器(_hover的值为&:is(:hover, [data-hover]))sources,指出每个令牌来自哪个预设或文件,tokens[path].source会索引到此处themes,以及项目定义时才会包含的recipes、slotRecipes、patterns、textStyles、layerStyles、animationStyles、keyframes和colorPalettes。空部分会被省略。
当你需要查询令牌的值、其 CSS 变量或定义它的预设时,请读取此文件。如果只想快速确认某个名称是否存在,
<outdir>/types/tokens.d.ts 中的联合类型更轻便,也更容易用 grep 搜索。
如果找不到该文件,通常是因为没人传入 --spec,而不是 codegen 失败了。运行
panda codegen --spec。不要改为读取 panda.config.ts:它记录的是有人编写的内容,而不是
Panda 合并预设后解析出的结果;启用 @pandacss/preset-base 和 @pandacss/preset-panda 后,大部分令牌集
都不会出现在其中。
如果文件存在但已过时,那是因为它由先前的运行生成。它不会自动刷新,因此请再次运行
panda codegen --spec。
审查主题时,从这里开始。source 为预设的令牌属于继承内容,因此项目自己的新增项是那些
source 为本地来源的令牌,而差异通常就出现在这里。
根据它编写 DESIGN.md
如果团队想为 Agent 提供一份可读的说明,请基于此文件生成,而不要手动编写。规范提供了所有
事实:令牌名称、值、条件、配方变体和来源信息。它无法提供的是意图,也就是何时该用 fg.muted 而不是 fg、哪些比例不可使用,以及品牌有哪些禁忌。
编写意图,生成其余内容,并在文件中注明它来自哪次规范生成。不要在说明文字中手动复制令牌值。
下次有人编辑主题时,这些值就会过时;而重新生成只需运行一次 panda codegen --spec。
如果项目希望定期生成此文件,应将 --spec 加入构建脚本,而不要依赖某人记得传入此标志。
何时将样式升级为配方
逐级升级。每次只升一级,并且仅在测试通过时升级。
调用处的 css({ … })。 一切从这里开始。样式只用一次时,就保持这样。
↓ 同一个样式对象出现在三个或更多位置
组件文件中的 cva。 一个包含变体的本地配方,与唯一的使用者放在一起。
import { cva } from '../styled-system/css'
const button = cva({
base: { px: '4', py: '2', rounded: 'md' },
variants: {
visual: { solid: { bg: 'accent' }, outline: { borderWidth: '1px' } },
size: { sm: { fontSize: 'sm' }, md: { fontSize: 'md' } },
},
defaultVariants: { visual: 'solid', size: 'md' },
})
↓ 此文件之外的使用者需要这些变体,或者这些样式属于设计系统而不是组件
配置配方。 在 panda.config.ts 的 theme.recipes 下使用 defineRecipe。此时变体 API 就成为系统的一部分,
类型会生成,CSS 也会从配置中输出。
↓ 组件渲染了多个元素,且每个元素都需要自己的样式
插槽配方。 在 theme.slotRecipes 下使用 defineSlotRecipe,或在文件中使用 sva。
不需要升级时就别升级。只在一个文件中使用的样式却定义为配置配方,会增加间接层和类型开销。
适合使用槽位配方的情况
如果组件会渲染可供使用者独立设置样式或替换的部分,请使用槽位:root、trigger、content、title、 indicator。判断信号:
- 你正在将
className传递给内部元素以设置样式。 - 你编写了嵌套选择器,深入访问子组件的标记。
- 两个元素需要以不同方式响应同一个变体。
const card = sva({
slots: ['root', 'header', 'body'],
base: {
root: { rounded: 'lg', borderWidth: '1px' },
header: { px: '4', py: '3', borderBottomWidth: '1px' },
body: { px: '4', py: '4' },
},
variants: {
density: {
compact: { header: { py: '2' }, body: { py: '2' } },
},
},
})
先设计组件结构。编写样式前先命名槽位。槽位列表是公开 API,因此之后重命名会破坏所有使用者。
将配方转换为槽位配方: 列出组件渲染的元素,为每个元素命名,将每个样式块移到对应槽位下,然后移动变体,让每个变体指定它作用的槽位。base 和每个变体都要从扁平样式对象改为以槽位为键的映射。然后将使用者从 createRecipeContext 切换为 createSlotRecipeContext。
在 JSX 中使用配方(v2)
createRecipeContext(recipe)会为cva配方返回{ withContext }。createSlotRecipeContext(recipe)会为sva返回{ withRootProvider, withProvider, withContext }。withRootProvider用于自身不渲染槽位的 root。
两者都来自 <outdir>/jsx。v1 中的单一 createStyleContext 已移除。
设计变体 API
- 按意图而非实现命名变体。 使用
visual、size、tone,不要用isBlue。 - 始终设置
defaultVariants。 否则,不传任何值的使用者只能得到基础样式。作为运行时 prop 传入的变体也只会生成基础样式和默认值。 - 布尔变体使用
true/false键。仅在确实只有两种情况时使用。 - 复合变体会增加 CSS。 每种组合都会生成 CSS。4 种视觉样式、4 种尺寸和 3 种色调组成的矩阵,最多会生成 48 个代码块。在 v2 中,复合变体会在构建时立即生成。设置
optimize.smartCompoundVariants: true,可将输出范围缩小到实际使用的组合。 - 优先增加变体,而不是复合变体。 如果矩阵中的大多数单元格都需要复合规则,说明维度设计不合理。
- 支持响应式变体值(
size={{ base: 'sm', md: 'md' }}),但仅适用于提取出的字面量。
对于启用了 isolatedDeclarations 的导出组件,请使用变体键进行注解,以免 CSS 进入 .d.ts:
import type { RecipeRuntimeFn } from '../styled-system/types'
export const button: RecipeRuntimeFn<{ visual?: 'solid' | 'outline' }> = cva({ … })
SlotRecipeRuntimeFn 和 StyledComponent<Tag, Props> 对 sva 和 styled 也有同样的作用。
反模式
- 主题只是堆在一起的基础值。 如果调用处都引用
gray.600,品牌重塑时就得在整个应用中查找并替换。这正是语义层要解决的问题。 - 在全局令牌中使用组件名称。
colors.buttonPrimary意味着未来每个组件都会发明自己的名称。应按角色命名,而非按使用者命名。 - 为每个组件重新定义尺度。 在
base中重新声明间距值的配方,正逐渐偏离主题。 - 在配方中编写页面布局。 配方用于组件。页面组合应使用
css()和 patterns。 - 在调用处区分深色模式。 在语义令牌中设置一个
_dark,胜过在组件中写五十个。 - 防御性地添加槽位。 不设置样式的槽位也是你必须维护的 API。
更改配置后
运行 panda codegen(或 panda build)。在此之前,令牌、配方及其类型都不会生成。然后检查 CSS 差异。令牌重命名后若某个值悄然丢失,会表现为相应 CSS 不再生成。
满足以下条件即表示正常运行
- 品牌重塑只需调整配置,不必修改调用点。
_dark出现在theme.semanticTokens中,除此之外几乎不会出现。- 切换
dark类或data-panda-theme会改变页面,且没有任何组件读取其中任一项。 themes中的每个主题都已列在staticCss.themes中,或通过getTheme加载,因此不会有主题不生成任何内容。- 在组件中搜索
gray.600这样的原始值,几乎找不到结果。 - recipe 的
slots数组中的每个插槽都有对应的样式。 - 没有任何令牌名称包含组件名称。
panda codegen运行无误,且生成的 recipe CSS 与你计划发布的变体一致。
另请参阅
- 根据你设计的系统编写样式:调用 Skill 工具并输入 “panda-css”。
- v1 到 v2 迁移,包括
createStyleContext拆分:调用 Skill 工具并输入 “panda-css-migrate”。