emilkowalski/skills 拆解:把设计品味写进 AI Agent
最近很多 AI 编码工具都在讲 Skills,但大部分技能解决的是工程流程问题,比如怎么写测试、怎么做代码审查、怎么发布。emilkowalski/skills 有点不一样,它关心的是前端里最难被提示词说清楚的一块:界面品味。
项目作者是 Emil Kowalski,做过 Vercel 和 Linear 的设计工程,也维护过 Sonner 这类很强调手感的开源组件。截至 2026 年 7 月 2 日,这个仓库在 GitHub 上有 4.1k stars,MIT 协议,README 里的安装方式很直接:
bash
npx skills@latest add emilkowalski/skills
一句话概括这个项目:
它把设计工程师判断 UI 细节的方式,写成 AI Agent 可以执行的规则。
一、它解决的问题:Agent 会写界面,但不一定会做对细节
现在让 Agent 写一个弹窗、下拉菜单、toast 或按钮状态,通常不难。难的是它经常会做出一些"能跑但不对"的选择:
- 下拉菜单进入时用了
ease-in,导致第一眼反应慢; - 弹层从
scale(0)出现,看起来像凭空冒出来; - hover、键盘快捷键、命令面板这种高频操作也加动画;
- 用
transition: all,把不该动的属性也一起动画; - popover 从中心缩放,而不是从触发按钮的位置出现;
- 动画时长偏慢,让产品显得迟钝;
- 忘了
prefers-reduced-motion和触屏设备的 hover 限制。
这些问题单独看都很小。用户可能说不出哪里错,但会觉得界面不够顺、不够快、不够高级。
Emil 的判断是:Agent 缺的不是 CSS 语法,而是 taste。更准确一点说,它缺的是一组经过训练的、能落到代码细节里的设计判断。
二、仓库里只有三个技能,但分工很清楚
这个仓库很小,根目录下只有三个技能:
| 技能 | 用途 |
|---|---|
emil-design-eng |
主技能,覆盖动效、组件设计、手势、性能、可访问性和组件默认值 |
review-animations |
专门审查动画和 motion 代码,默认严格,必须给出 Before / After / Why 表格 |
animation-vocabulary |
把模糊的动画描述翻译成准确术语,方便和 AI 或设计师沟通 |
emil-design-eng 是主体。它不是一份"设计建议清单",更像一个设计工程师的判断系统:什么时候该动,为什么动,用什么缓动,多久,是否可中断,是否会掉帧,是否照顾减弱动态效果设置。
review-animations 则把这些判断变成审查流程。它定义了十条不可协商标准,比如动画必须有目的、高频动作不该动画、UI 动画尽量低于 300ms、只动画 transform 和 opacity、popover 要从触发点缩放、移动效果要支持 reduced motion。它的姿态很硬:通过是争取来的,不是默认给的。
animation-vocabulary 看起来最轻,但很实用。很多人不是不会描述动效,而是不知道那个东西叫什么。比如"那个 iOS 里拖到底会有阻尼再弹回来的感觉",技能会把它归到 rubber-banding;"列表项一个接一个出现",就是 stagger。术语一准,提示词和评审都会准很多。
三、这个技能具体怎么用
先安装:
bash
npx skills@latest add emilkowalski/skills
装完以后,它会变成一组给 Agent 读的技能文件,不是你手动运行的 npm 包。你在 Claude Code、Codex、Cursor 这类工具里做 UI 相关任务时,可以直接点名使用对应技能。
最常用的方式有三种。
1. 做组件时,让它先按设计工程规则实现
比如你正在写一个 Vue 里的 Popover,可以这样叫 Agent:
text
用 emil-design-eng 帮我改一下 src/components/BasePopover.vue。
要求:打开时从触发按钮的位置出现,动画要轻,不要影响键盘操作,支持 prefers-reduced-motion。
这个提示词比"让动画更自然"好很多。它直接指定了组件、交互目标、触发源、性能和可访问性约束。emil-design-eng 会把这些要求翻译成具体实现,比如 transform-origin、ease-out、scale(0.96)、opacity、@media (prefers-reduced-motion: reduce)。
2. 写完以后,用 review-animations 做一次严格评审
你也可以不让它直接改代码,而是先审:
text
用 review-animations 审查 src/components/BaseDropdown.vue 里的动画,只看 motion 相关问题。
请按 Before / After / Why 表格输出,不要改代码。
这个技能适合放在 PR 前。它不会泛泛说"动画可以优化",而是会抓具体问题,比如:
| Before | After | Why |
|---|---|---|
transition: all 300ms ease-in |
transition: transform 180ms cubic-bezier(0.23, 1, 0.32, 1), opacity 180ms cubic-bezier(0.23, 1, 0.32, 1) |
all 会误伤属性,ease-in 会让进入动画开头变慢 |
transform: scale(0) |
transform: scale(0.96); opacity: 0 |
弹层不该从完全不存在的状态出现 |
transform-origin: center |
transform-origin: var(--popover-transform-origin) |
Popover 应该从触发按钮的位置长出来 |
你可以先看评审表,再决定让 Agent 改哪几项。
3. 说不清动效名字时,用 animation-vocabulary 找词
很多时候你不是不会做,而是不知道怎么准确描述。比如你可以问:
text
用 animation-vocabulary:iOS 那种拖到边界会有阻力,松手再弹回来的动效叫什么?
它会给你 Rubber-banding。再比如:
text
一个列表里的卡片不是一起出现,而是一个接一个淡入上移,这叫什么?
它会给你 Stagger。这个技能适合用在写需求、写 prompt、跟设计师对齐术语的时候。
四、实际案例一:修一个 Popover 动画
假设 Agent 原来给你写了这样一段 Vue transition:
css
.popover-enter-active,
.popover-leave-active {
transition: all 300ms ease-in;
}
.popover-enter-from,
.popover-leave-to {
opacity: 0;
transform: scale(0);
}
这段代码能跑,但手感会差。问题有三个:transition: all 太粗,ease-in 让进入动画起步慢,scale(0) 让弹层像凭空冒出来。
用 review-animations 审完以后,更合理的版本大概会变成这样:
css
.popover {
transform-origin: var(--popover-transform-origin, top center);
}
.popover-enter-active,
.popover-leave-active {
transition:
transform 180ms cubic-bezier(0.23, 1, 0.32, 1),
opacity 180ms cubic-bezier(0.23, 1, 0.32, 1);
}
.popover-enter-from,
.popover-leave-to {
opacity: 0;
transform: scale(0.96);
}
@media (prefers-reduced-motion: reduce) {
.popover-enter-active,
.popover-leave-active {
transition: opacity 120ms ease;
}
.popover-enter-from,
.popover-leave-to {
transform: none;
}
}
这里的变化不是"加点高级感",而是每一处都有理由:
- 只动画
transform和opacity,避免 layout 和 paint; - 进入动画用强一点的
ease-out,用户点击后立刻看到反馈; - 从
scale(0.96)开始,保留一点体积感; transform-origin留给组件根据触发按钮位置动态设置;- reduced motion 下保留淡入淡出,去掉位移动作。
这类案例很适合组件库。Popover、Dropdown、Tooltip、ContextMenu 都可以用同一套检查逻辑。
五、实际案例二:Toast 为什么不要随便用 keyframes
Toast 看起来简单,但它是很容易被快速触发的组件。比如用户连续保存、连续复制、连续报错,toast 会频繁加入和移除。
一个常见写法是这样:
css
.toast {
animation: toast-in 400ms ease-in-out;
}
@keyframes toast-in {
from {
opacity: 0;
transform: translateY(24px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
问题在于 keyframes 不适合频繁中断和重新定位的 UI。新的 toast 插入、旧的 toast 上移、用户鼠标移入暂停、滑动关闭,这些都要求动画能从当前状态继续调整,而不是每次从 0 开始播。
emil-design-eng 会更偏向 transition 或 spring:
css
.toast {
opacity: 1;
transform: translateY(0);
transition:
transform 220ms cubic-bezier(0.23, 1, 0.32, 1),
opacity 180ms ease;
}
.toast[data-entering],
.toast[data-leaving] {
opacity: 0;
transform: translateY(12px);
}
.toast:active {
transform: scale(0.98);
}
如果是可拖拽关闭的 toast,它还会提醒你看速度,而不是只看距离。用户快速一甩,即使没拖过一半,也应该能关闭。这就是技能里提到的 momentum dismissal。
区别就在这里:它不只给一组动画参数,还会考虑这个组件在真实产品里会被怎样使用。
六、实际案例三:按钮、Tooltip 和高频操作
有些动效该加,有些动效该删。这个技能最有用的地方之一,就是帮你判断频率。
按钮按下反馈适合加,因为它是用户直接操作后的即时反馈:
css
.button {
transition: transform 140ms cubic-bezier(0.23, 1, 0.32, 1);
}
.button:active {
transform: scale(0.97);
}
Tooltip 的逻辑更细一点。第一次 hover 可以有延迟,避免用户路过时误触发;但当一个 tooltip 已经打开,用户移动到相邻按钮时,后续 tooltip 应该立刻出现,不要每个都等一遍。
text
用 emil-design-eng 检查 Toolbar 的 tooltip 体验:
第一次 hover 延迟打开,后续相邻 tooltip 立即打开,触屏设备不要触发 hover 动画。
相反,命令面板、快捷键触发、键盘上下移动列表,这些高频操作通常应该删动画。用户一天可能打开命令面板几十次甚至上百次,这时候每多 150ms 都会变成阻力。
这条规则对后台产品很实用。SaaS、CRM、开发工具、管理系统里的 UI 不需要处处有动效,真正重要的是响应快、状态清楚、不会打断连续操作。
七、最核心的方法:把品味拆成决策树
这个项目最值得学的地方,不是某个具体动画参数,而是它把"感觉对不对"拆成了可执行的判断。
比如动画要不要出现,它先问一个非常工程化的问题:这个动画用户一天会看到多少次?
- 键盘快捷键、命令面板这种一天可能触发上百次的动作,不动画;
- hover、列表导航这种一天几十次的动作,删掉或大幅减弱;
- 弹窗、抽屉、toast 这种偶尔出现的界面,可以做标准动画;
- 首次引导、庆祝反馈、营销演示这种低频场景,可以加一点愉悦感。
这比"动画要克制"有用得多。克制是态度,频率表是执行规则。Agent 拿到后不需要猜,它只要判断场景属于哪一类。
缓动也是同样的处理方式:
- 元素进入或退出,用
ease-out,因为一开始就要给用户反馈; - 屏幕上已有元素移动或变形,用
ease-in-out; - hover 或颜色变化,用普通
ease; - 跑马灯、进度条这种匀速运动,才用
linear; - UI 交互里避免
ease-in,因为它开头慢,用户最关注的那一刻反而没反应。
再比如 scale(0)。很多 Agent 会把弹层初始状态写成完全缩小到 0,这在代码里很自然,但在视觉上不自然。Emil 的规则是从 scale(0.9) 到 scale(0.97) 这类仍有体积感的状态开始,再配合透明度。原因很直观:现实世界里的东西不会从完全不存在突然变出来。
这就是所谓"品味被程序化"的关键:把审美从玄学拉回具体判断,不断追问"为什么这个选择感觉更好",再把答案写成规则、阈值、反例和输出格式。
八、它对前端开发最有价值的地方
前端开发里有很多判断不是类型系统能兜住的。按钮有没有按压反馈,tooltip 第二次 hover 是否应该跳过延迟,drawer 拖拽越界时是硬停还是加阻尼,toast 堆叠时用 keyframes 还是 transition,这些都不会让构建失败,但会决定产品手感。
emilkowalski/skills 对前端开发的价值在于,它把这些隐性判断放进了 Agent 的默认工作流。
你让 Agent 改一个 popover 动画,它不只是"让它更顺滑",而是会检查:
- 这个交互频率高不高;
- 动画是否有明确目的;
- 时长是否超过 UI 动画的合理范围;
- 是否用了
transition: all; - 是否从触发源位置缩放;
- 是否只动了 GPU 友好的属性;
- 是否支持 reduced motion;
- 触屏设备上 hover 是否会误触发。
这套检查对组件库、SaaS 后台、设计系统、动效密集型前端都很有用。它特别适合那些"功能已经做完,但总觉得不够像一个成熟产品"的阶段。
九、它比普通提示词更具体
普通提示词经常写成这样:
请让 UI 更精致,动画更自然,注意性能和可访问性。
这类话对人类也许有启发,对 Agent 不够。因为它不知道"自然"是什么,也不知道"注意性能"具体要检查哪些属性。
这个仓库的写法更像代码审查标准:
- 先定义适用场景;
- 再定义判断顺序;
- 给出具体阈值,比如 100 到 160ms、150 到 250ms、UI 动画尽量低于 300ms;
- 给出危险信号,比如
transition: all、scale(0)、ease-in、布局属性动画; - 给出修复优先级,能删就删,不能删再减弱,再改缓动、改 origin、改性能;
- 强制输出 Before / After / Why 表格,让评审可读、可执行。
这也是它和很多"AI 设计建议"最大的区别。它不靠一句"提升品味",而是把品味拆成一组不会轻易走样的检查项。
十、这个项目也有边界
第一,它很有作者风格。Emil 的动效观偏向高质感、克制、响应快、少装饰。这对大多数产品界面是好事,但不等于所有品牌都应该照抄。游戏、儿童产品、创意展示站、强品牌营销页,可能需要更强的表现力。
第二,它主要覆盖设计工程和 motion,并不替代完整的产品设计流程。信息架构、业务流程、可用性研究、视觉品牌系统,仍然需要另外的上下文。
第三,它要求 Agent 真的会遵循长规则。能力弱的模型可能会读完规则,但实现时还是回到默认写法。这个问题不只属于这个仓库,所有 Skills 都会遇到。
第四,技能不是设计师。它能把已知经验稳定复用,但新的判断、复杂取舍、品牌方向,仍然需要人来拍板。
十一、我会怎么用它
如果你是前端开发,我建议不要把它当成"装了就自动变高级"的魔法包,而是当成一个可复用的 UI 评审助手。
比较适合的用法有三种。
第一种,用它审查组件库里的动效。比如 Dialog、Popover、Dropdown、Toast、Tooltip、Drawer,这些组件的细节一旦定下来,会被整个产品反复复用,值得提前把手感调对。你可以先让 review-animations 只出评审表,再挑几项让 Agent 改。
第二种,用它给 Agent 补默认品味。让 Agent 实现交互前,先加载 emil-design-eng;实现完再用 review-animations 审一遍。比如"给 BaseDropdown 加动效"这类任务,不要只说"顺滑一点",而是把频率、触发方式、reduced motion、键盘操作都写进去。
第三种,把它当成自己写技能的范本。你可以照着它的结构,把团队自己的设计规范写成类似规则:表单错误怎么出现,空状态怎么处理,数据表格怎么加载,按钮 loading 怎么变形,哪些场景不能加动效。比如团队规定所有后台弹窗都不做 bounce,所有列表加载都用 skeleton,不用 shimmer;这些都可以写成技能。
十二、为什么这个项目值得分享
emilkowalski/skills 讨论的不是"AI 会不会设计"这种大问题,它关心的是一个更实际的问题:
如果你已经知道什么是好设计,能不能把这些判断交给 Agent 稳定执行?
这个问题对前端尤其重要。因为前端的很多质量差异,不在功能层,而在无数细节层。一个按钮按下去有没有物理反馈,一个菜单从哪里长出来,一个动画是不是慢了 100ms,一个拖拽越界是不是有阻尼,这些决定了产品是"能用",还是"有手感"。
Emil 的答案很直接:品味可以训练,也可以表达。表达清楚以后,就可以变成技能。
这也是我觉得这个仓库最值得借鉴的地方。它提醒我们,未来写 Skills 不只是把流程写给 Agent,也可以把专业判断写给 Agent。前者让 AI 少犯工程错误,后者让 AI 更接近一个靠谱的同行。
参考资料
- GitHub 仓库:github.com/emilkowalsk...
- 项目主页:emilkowal.ski/skill
- Agents with Taste:emilkowal.ski/ui/agents-w...
- Developing Taste:emilkowal.ski/ui/developi...
- animations.dev:animations.dev/