airi 项目 UnoCSS 实战:用 transformerDirectives 在 CSS 中原生启用 @apply、@screen、theme() 与 icon()
transformerDirectives 是 UnoCSS 提供的一个核心 transformer,它让开发者能够直接在 CSS 中书写 @apply、@screen、theme() 与 icon() 四种指令,把工具类(utilities)与主题(theme)能力"搬进"普通 CSS 文件中。在 airi 仓库中,这条能力既被根配置集中启用(uno.config.ts),也被应用到桌面端渲染层(如 spotlight.vue)的复杂渐变与响应式样式编写上。读完本文,你将掌握四种指令的完整语法、别名配置方法,以及如何像 airi 一样在 Vue SFC 的 scoped style 里优雅地混用工具类与原生 CSS。
本文的原始知识卡位于仓库 .agents/skills/unocss/references/transformer-directives.md,下面结合 airi 的真实配置与源码逐层展开。
transformerDirectives 解决的问题
在纯 UnoCSS 工作流中,工具类通常直接写在模板的 class 属性中。但当你需要复用一组工具类、或想在 ::before / ::after 等伪元素、@keyframes 内部、媒体查询里组合样式时,模板 class 就无能为力了。transformerDirectives 正是为此而生——它扫描 CSS 源码,把指令转换为展开后的工具类声明,使"样式逻辑收敛到 CSS"与"工具类按需生成"两种范式可以共存。
安装与最小配置非常简单,在 UnoCSS 配置文件(通常是 uno.config.ts)中加入 transformer 即可:
import { defineConfig, transformerDirectives } from 'unocss'
export default defineConfig({
transformers: [
transformerDirectives(),
],
})
airi 中的启用方式:从根配置到各应用共享
airi 的根级 uno.config.ts 把共享配置封装为 sharedUnoConfig(),其中 transformers 同时启用了 directives 与 variant group 两个转换器,并对别名做了显式限定:
// 根 uno.config.ts(节选)
transformers: [
transformerDirectives({
applyVariable: ['--at-apply'],
}),
transformerVariantGroup(),
],
这里的关键点:显式传入 applyVariable: ['--at-apply'] 后,自定义属性别名只保留 --at-apply 一个(不再包含默认同义别名,详见下文别名一节)。随后,不同子应用通过 mergeConfigs 复用这份共享配置——例如 packages/stage-ui/uno.config.ts 直接合并了根配置:
import { defineConfig, mergeConfigs } from 'unocss'
import { histoireUnoConfig, sharedUnoConfig } from '../../uno.config'
export default mergeConfigs([
sharedUnoConfig(),
histoireUnoConfig(),
defineConfig({}),
])
也有的应用独立声明完整配置,例如 apps/component-calling/uno.config.ts 中与 transformerVariantGroup() 并排启用了默认形态的 transformerDirectives()(不传参数)。仓库通过 pnpm workspace catalog 统一锁定了 unocss 版本(见 pnpm-workspace.yaml 中 unocss: 66.7.5 的 catalog 条目),因此各应用获得的 transformer 行为是一致的。
@apply:在 CSS 中内联工具类
基础用法与带变体的写法
@apply 可以把一组工具类合并进任意 CSS 规则中,最直接的应用是封装"类组件化"的样式:
.custom-btn {
@apply py-2 px-4 font-semibold rounded-lg;
}
当需要混入 hover:、focus: 等**变体(variant)**时,由于变体值中带冒号与空格,必须使用引号包裹整个字符串,否则指令解析会失败:
.custom-btn {
@apply 'hover:bg-blue-600 focus:ring-2';
}
CSS 自定义属性替代写法(vanilla CSS 兼容)
@apply 是标准 CSS 之外的指令语法,若希望样式文件仍能通过普通 CSS 解析器(例如防止原生 CSS 校验报错,或希望渐进增强),可改用 CSS 自定义属性别名:
.custom-div {
--at-apply: text-center my-0 font-medium;
}
transformer 支持三个同义别名,配置如下:
transformerDirectives({
applyVariable: ['--at-apply', '--uno-apply', '--uno'],
// 或完全关闭别名能力:applyVariable: false
})
- 默认别名是
--at-apply; - 当传入数组时,数组内容即生效别名集合;
- 设为
false可关闭该特性(若你的样式表中恰好有用到这些自定义属性的场景,可避免误展开)。
airi 的真实用法:伪元素 + 复杂渐变 + 深色模式
自定义属性写法在 airi 桌面端被大量使用,因为它能出现在 ::before / ::after 这类伪元素规则中,与手写 position、mask-image 等原生属性无缝共存。下面摘自已搜索可见的 apps/stage-tamagotchi/src/renderer/pages/spotlight.vue(舞台 spotlight 输入框的光晕层):
.spotlight-card::before {
pointer-events: none;
--at-apply: 'bg-gradient-to-r from-primary-500/25 via-primary-500/12 to-transparent dark:from-primary-400/25 dark:via-primary-400/12 dark:to-transparent';
content: '';
position: absolute;
inset: 0;
z-index: 0;
width: 85%;
height: 100%;
mask-image: linear-gradient(120deg, white 100%);
}
.spotlight-card::after {
pointer-events: none;
--at-apply: 'bg-dotted-[primary-300/35] dark:bg-dotted-[primary-200/16]';
position: absolute;
inset: 0;
z-index: 0;
width: 100%;
height: 100%;
background-size: 10px 10px;
content: '';
mask-image: linear-gradient(165deg, white 30%, transparent 55%);
}
这个例子至少说明了三点实战要点:
- 值必须带引号:多段工具类(含空格与
dark:变体)必须写成引号字符串,--at-apply后的值才会被当作完整指令解析; - 可以消费自定义规则与主题色:
bg-dotted-[primary-300/35]对应根配置 uno.config.ts 中自定义的bg-dotted-[...]规则,而primary-300等色板来自presetChromatic生成的主题——证明--at-apply展开的是与模板 class 完全一致的完整工具类体系; - 原生 CSS 与指令可并存:同一规则内
mask-image、background-size、position等手写声明与--at-apply互不干扰,这让"UI 引擎层样式"保持为可读的 CSS。
另一个更简洁的用法见 apps/stage-tamagotchi/src/renderer/pages/index.vue(加载动效墙):
.wall {
--at-apply: text-primary-300;
--wall-width: 8px;
animation: wall-move 1s linear infinite;
background-image: repeating-linear-gradient(
45deg,
currentColor,
currentColor var(--wall-width),
#ff00 var(--wall-width),
#ff00 calc(var(--wall-width) * 2)
);
}
这里 --at-apply: text-primary-300 仅为元素注入文字颜色,随后手写的 background-image 通过 currentColor 引用它——指令与原生 CSS 通过颜色关键字协同工作,是"工具类用于取主题 token、原生 CSS 负责复杂绘制"的理想分工。
提示:从源码检索看,airi 的 Vue SFC 中统一采用
--at-apply别名而非@apply关键字。若团队规约更看重可迁移性,可采用与根配置一致的applyVariable显式列表,避免样式被其他解析器误读。
@screen:把断点写成语义化的媒体查询
@screen <断点名> 会被转换为对应的媒体查询,断点名称来自主题 breakpoints(presetWind3 / preset-mini 默认提供 sm/md/lg/xl/2xl 等)。经典响应式栅格示例:
.grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
}
@screen sm {
.grid {
grid-template-columns: repeat(3, 1fr);
}
}
@screen lg {
.grid {
grid-template-columns: repeat(4, 1fr);
}
}
相比手写媒体查询,@screen 的可读性更强,且断点值始终与主题定义保持单一事实来源。
断点变体:lt- 与 at-
除默认的"大于等于某断点"语义外,还支持两种变体:
/* 小于某断点才生效(max-width 语义) */
@screen lt-sm {
.item { display: none; }
}
/* 仅在该断点区间生效(min/max 组合) */
@screen at-md {
.item { width: 50%; }
}
这在"移动端隐藏某元素""仅中屏横排"等场景中比默认的向上兼容语义更精确。
theme():在任意 CSS 属性中读取主题令牌
theme('路径点分字符串') 允许在 CSS 值里直接读取 UnoCSS 主题配置,避免把 colors.blue.500 这类十六进制硬编码散落在样式文件中:
.btn-blue {
background-color: theme('colors.blue.500');
padding: theme('spacing.4');
border-radius: theme('borderRadius.lg');
}
取值路径采用点分记号,映射到根配置 defineConfig({ theme: {...} }) 的对象结构。airi 根配置扩展了丰富的主题段(见 uno.config.ts),例如 theme.fontFamily 中定义的 cute / cutejp / sans-rounded 等字体族、以及 theme.animation 下一整套 keyframes / durations / timingFns 的入场退场动画配置——这些都可以通过 theme('fontFamily.cute')、theme('animation.durations.fadeIn') 这类写法在 CSS 中引用,与 @apply、模板 class 读取的是同一份主题数据。
icon():把图标工具类转成 SVG 背景图
icon() 指令能把预设图标工具类(如 i-carbon-sun)转换为内联 SVG background-image。它依赖 preset-icons 提供图标解析——airi 根配置恰好启用了 presetIcons 并挂载了多个 Iconify 集合(见 uno.config.ts):
.icon-sun {
background-image: icon('i-carbon-sun');
}
/* 第二个参数指定自定义颜色 */
.icon-moon {
background-image: icon('i-carbon-moon', '#fff');
}
/* 颜色参数同样支持 theme() 读取主题色 */
.icon-alert {
background-image: icon('i-carbon-warning', 'theme("colors.red.500")');
}
注意两点:颜色的使用方式是覆盖图标原色(本质是为内联 SVG 注入指定色值);而 theme("colors.red.500") 之所以用双引号,是因为它嵌套在外层单引号之内,且颜色参数本身是一个表达式而非普通字符串。该能力非常适合为那些无法通过 text-* 染色的背景型图标注入主题色。
完整组合示例:Card 组件
把四种指令放进一个组件级样式,即可看到它们如何协作(完整示例亦保留于原始知识卡):
.card {
@apply rounded-lg shadow-md p-4;
background-color: theme('colors.white');
}
.card-header {
@apply 'font-bold text-lg border-b';
padding-bottom: theme('spacing.2');
}
@screen md {
.card {
@apply flex gap-4;
}
}
.card-icon {
background-image: icon('i-carbon-document');
@apply w-6 h-6;
}
从这份示例可以归纳协作模式:@apply 负责排版骨架与可见性,theme() 负责取值,@screen 负责响应式分段,icon() 负责图形资产。在 airi 这类多端(Web / Electron / Capacitor / tamagotchi 桌面应用)monorepo 中,把这份 CSS 放在公共 UI 包(如 packages/stage-ui)内,配合根配置的分层启用策略,即可保证各端获得一致的指令行为。
使用建议与注意事项
- 作用域内使用:在 Vue SFC 中,
<style scoped>内的@apply/--at-apply展开发生在 UnoCSS 构建阶段,Vue 的 scoped 属性处理与其并不冲突,airi 的多个页面即采用这种组合。 - 带空格与变体务必加引号:
@apply与--at-apply的值若包含hover:/dark:/ 空格分隔的多段类名,必须整体加引号,否则会被当作单一工具类解析失败。 - 别名的统一管理:既然项目从根配置统一了
applyVariable,新增子应用时应优先复用sharedUnoConfig()或与其保持一致,避免不同应用间别名集合漂移。 - 图标类需存在集合:
icon()依赖presetIcons及其配置的 collections;如果图标名不在已加载集合内,构建期不会生成对应背景图。 - 与 extractor 的配合:
@apply/--at-apply等指令内出现的工具类由 transformer 收集并触发生成,airi 根配置的 content pipeline(见 uno.config.ts)同时覆盖.vue、.ts/.js等文件,确保 CSS 中引用的类与模板中用到的类都进入统一扫描范围。
若需进一步了解相关的 preset-icons、preset-wind3、variant-group 与 shortcuts 能力,可继续查阅仓库 .agents/skills/unocss 下的 references 知识卡(如 preset-icons.md、preset-wind3.md、transformer-variant-group.md、core-shortcuts.md),它们与本文共同构成 UnoCSS 在 airi 中的完整使用参考。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0631
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
video-shotcraftAI宣传片skill,使用 Remotion 制作电影级产品视频:提供106 张镜头配方卡和可复用的视频魔板。适用于 Claude Code 与 Codex以及所有其他智能体Markdown00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python09
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00