关于 stitch-animate
stitch-animate 是为现有 Stitch 生成组件添加动效的设计技能,面向需要完善界面反馈和页面动效的开发者。它按微观、中观和宏观层级选择动画,并要求支持减少动态效果偏好。
主要功能
- 01为现有组件添加有目的的动效
- 02按三个层级规划动画
- 03支持 CSS、Framer Motion 与 Svelte 过渡
- 04遵循减少动态效果设置
- 05提供常见界面的动画模式
适用场景
- 01为按钮和链接增加交互反馈
- 02为卡片、弹窗添加进入动画
- 03为页面或区块添加过渡效果
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ gabelul/ stitch-kit --skill stitch-animate安装后在 Claude Code 中输入/stitch-animate确认已加载。
附带文件resources/animation-patterns.md
详细介绍
Stitch 动画层
你是一名动效设计工程师。你要为现有的 Stitch 生成组件添加恰到好处的动画,而不是重建组件。你的输出会在适当的时机为组件增添合适的动效,并始终兼容 prefers-reduced-motion。
请在生成组件之后运行此技能(stitch-nextjs-components 或 stitch-svelte-components),不要提前运行。
何时使用此技能
在以下情况下使用此技能:
- 组件已生成且正常运行,但显得静态
- 用户提到“动画”“过渡”“动效”“悬停效果”或“滚动显现”
- Stitch 设计截图清楚地体现了动效意图(重叠元素、Hero 区块、仪表板)
- 为已完成的组件集增添精致感
三个动效层级
先分析设计。按层级分配动画,不要让所有元素都动起来:
| 层级 | 用途 | 时长 | 缓动 | 示例 |
|---|---|---|---|---|
| 微动效 | 交互元素的悬停、焦点、激活状态 | 100–200ms | ease-out | 按钮悬停、链接颜色、图标缩放 |
| 中等动效 | 进入或离开视口的 UI 元素 | 250–400ms | cubic-bezier(0,0,0.2,1) | 卡片显现、侧边栏滑入、模态框打开 |
| 宏观动效 | 整个页面或区块的过渡 | 400–600ms | ease-in-out | 路由过渡、Hero 区块、引导流程 |
**经验法则:**不确定时,使用微动效。过度动画比没有动画更糟。
第 1 步:检查组件
阅读生成的组件文件。对每个组件,识别:
- 需要微动效的交互元素(按钮、链接、输入框、切换开关、带有
onClick的卡片) - 适合中等动效的显现元素(页面区块、卡片网格、侧边栏、模态框、抽屉、Toast)
- 适合宏观动效的 Hero 或标志性元素(主标题、特色图片、页面级过渡)
只为有明确用途的元素添加动画。如果无法用一句话解释某个元素为什么要动,就不要为它添加动画。
第 2 步:检测框架并选择动画方案
阅读 package.json 以确定框架,然后使用相应方案:
| 框架 | 方案 |
|---|---|
| Next.js / React | CSS + 可选的 Framer Motion |
| SvelteKit / Svelte | Svelte 内置过渡 + CSS |
| Vanilla HTML | 仅 CSS |
方案 A:CSS 过渡和动画(通用)
微动效和简单的中等动效使用 CSS。无需添加依赖。
微动效 — 交互状态
将以下内容添加到 design-tokens.css 或组件的 CSS 中:
/* Base transition shorthand — use on all interactive elements */
.transition-base {
transition:
background-color var(--motion-duration-fast) var(--motion-ease-default),
color var(--motion-duration-fast) var(--motion-ease-default),
border-color var(--motion-duration-fast) var(--motion-ease-default),
box-shadow var(--motion-duration-fast) var(--motion-ease-default),
transform var(--motion-duration-fast) var(--motion-ease-default),
opacity var(--motion-duration-fast) var(--motion-ease-default);
}
/* Button micro-interaction */
.btn {
transition: transform 150ms ease-out, box-shadow 150ms ease-out, background-color 150ms ease-out;
}
.btn:hover { transform: translateY(-1px); box-shadow: var(--shadow-md); }
.btn:active { transform: translateY(0); box-shadow: var(--shadow-sm); }
/* Card lift */
.card {
transition: transform 200ms ease-out, box-shadow 200ms ease-out;
}
.card:hover { transform: translateY(-4px); box-shadow: var(--shadow-lg); }
中等动效 — 元素显现
使用带有 animation-fill-mode: both 的关键帧动画:
@keyframes fade-up {
from { opacity: 0; transform: translateY(16px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes fade-in {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes slide-in-right {
from { opacity: 0; transform: translateX(24px); }
to { opacity: 1; transform: translateX(0); }
}
.animate-fade-up { animation: fade-up var(--motion-duration-base) var(--motion-ease-out) both; }
.animate-fade-in { animation: fade-in var(--motion-duration-fast) var(--motion-ease-out) both; }
.animate-slide-in-r { animation: slide-in-right var(--motion-duration-base) var(--motion-ease-out) both; }
/* Stagger children with CSS custom property */
.stagger-children > * {
animation-delay: calc(var(--stagger-index, 0) * 60ms);
}
prefers-reduced-motion(必需)
始终在每个动画 CSS 块的末尾添加此覆盖规则:
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}
方案 B:Framer Motion(React / Next.js)
在 React 项目中,使用 Framer Motion 实现中等和宏观动效。它通过 useReducedMotion 原生处理 prefers-reduced-motion。
安装
npm install framer-motion
滚动触发展现(最常见的用例)
'use client'
import { motion, useReducedMotion } from 'framer-motion'
/**
* Wraps children in a scroll-triggered fade+rise animation.
* Automatically disables animation when prefers-reduced-motion is active.
*/
export function RevealOnScroll({ children, delay = 0 }: {
children: React.ReactNode
delay?: number
}) {
const shouldReduce = useReducedMotion()
return (
<motion.div
initial={shouldReduce ? false : { opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: '-50px' }}
transition={{
duration: 0.4,
ease: [0, 0, 0.2, 1],
delay,
}}
>
{children}
</motion.div>
)
}
交错排列的卡片网格
'use client'
import { motion, useReducedMotion } from 'framer-motion'
const container = {
hidden: { opacity: 0 },
show: {
opacity: 1,
transition: { staggerChildren: 0.08 }
}
}
const item = {
hidden: { opacity: 0, y: 16 },
show: { opacity: 1, y: 0, transition: { ease: [0, 0, 0.2, 1], duration: 0.35 } }
}
export function AnimatedGrid({ cards }: { cards: CardProps[] }) {
const shouldReduce = useReducedMotion()
if (shouldReduce) {
return <div className="grid">{cards.map(c => <Card key={c.id} {...c} />)}</div>
}
return (
<motion.div className="grid" variants={container} initial="hidden" whileInView="show" viewport={{ once: true }}>
{cards.map(c => (
<motion.div key={c.id} variants={item}>
<Card {...c} />
</motion.div>
))}
</motion.div>
)
}
页面过渡包装器(App Router)
// app/template.tsx — wraps every page with a transition
'use client'
import { motion } from 'framer-motion'
export default function Template({ children }: { children: React.ReactNode }) {
return (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2 }}
>
{children}
</motion.div>
)
}
方案 C:Svelte 过渡(Svelte / SvelteKit)
对于 Svelte 项目,Svelte 内置过渡是最简洁的选择,无需添加依赖。
使用 Intersection Observer 实现滚动显现
Svelte 没有内置的滚动显现功能,但使用 use: 指令可以简洁地实现:
<script lang="ts">
import { fade, fly } from 'svelte/transition'
import { cubicOut } from 'svelte/easing'
/**
* Svelte action that triggers a fade-up animation when the element
* enters the viewport. Respects prefers-reduced-motion.
*/
function revealOnScroll(node: HTMLElement) {
const prefersReduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches
if (prefersReduced) return {}
node.style.opacity = '0'
node.style.transform = 'translateY(16px)'
const observer = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting) {
node.style.transition = `opacity 400ms cubic-bezier(0,0,0.2,1), transform 400ms cubic-bezier(0,0,0.2,1)`
node.style.opacity = '1'
node.style.transform = 'translateY(0)'
observer.unobserve(node)
}
},
{ threshold: 0.1, rootMargin: '-50px' }
)
observer.observe(node)
return {
destroy() { observer.disconnect() }
}
}
</script>
<section use:revealOnScroll>
<h2>This section fades in on scroll</h2>
</section>
列表项动画
<script lang="ts">
import { fly } from 'svelte/transition'
import { quintOut } from 'svelte/easing'
let items = $state<Item[]>([...])
</script>
{#each items as item, i (item.id)}
<div
in:fly={{ y: 16, duration: 300, delay: i * 60, easing: quintOut }}
out:fade={{ duration: 150 }}
>
<ItemCard {...item} />
</div>
{/each}
模态框 / 抽屉的进入与退出动画
<script lang="ts">
import { fade, fly } from 'svelte/transition'
let { isOpen = false } = $props()
</script>
{#if isOpen}
<div
class="backdrop"
transition:fade={{ duration: 200 }}
role="presentation"
/>
<aside
class="drawer"
transition:fly={{ x: 320, duration: 300, easing: cubicOut }}
role="dialog"
aria-modal="true"
>
{@render children()}
</aside>
{/if}
第 3 步:为现有组件添加动画
修改现有文件时:
- 先阅读每个组件文件——了解当前结构
- 仅为 Micro 层级添加 CSS 类(绝不要更改 Micro 的组件逻辑)
- 使用 motion 组件包裹 Meso/Macro 层级(React),或添加 transition 指令(Svelte)
- 如果主 CSS 文件中尚未添加 reduced-motion 覆盖规则,则添加该规则
- 测试两种状态——启用和禁用动画(使用浏览器 DevTools 模拟 reduced motion)
不要为以下内容添加动画
- 导航链接——仅使用颜色/下划线过渡
- 滚动行为——仅在适当情况下使用
scroll-behavior: smooth,并且仍需添加 reduced-motion 覆盖规则 - 数据表格——会分散阅读注意力;仅使用细微的行悬停效果
- 页面上的每个元素——每个屏幕选择 2-3 个重点动画
故障排除
| 问题 | 解决方法 |
|---|---|
| 动画未播放 | 检查动画触发前元素是否已存在于 DOM 中 |
| Framer Motion 水合错误 | 确保组件包含 'use client' 指令 |
| Svelte 过渡播放两次 | 检查开发模式下是否发生了重复渲染(相当于 StrictMode) |
| 动画卡顿/延迟 | 对动画元素谨慎添加 will-change: transform, opacity |
| reduced motion 未能停止动画 | 确保 @media (prefers-reduced-motion) 在动画 CSS 加载之后加载 |
参考资料
resources/animation-patterns.md——常见 UI 组件的即用型模式目录,可直接复制粘贴