mcpField
S
stitch-animate

stitch-animate

github.com/gabelul设计与 UI4743 次安装14 周前更新
代码仓库

关于 stitch-animate

stitch-animate 是为现有 Stitch 生成组件添加动效的设计技能,面向需要完善界面反馈和页面动效的开发者。它按微观、中观和宏观层级选择动画,并要求支持减少动态效果偏好。

主要功能

  1. 01为现有组件添加有目的的动效
  2. 02按三个层级规划动画
  3. 03支持 CSS、Framer Motion 与 Svelte 过渡
  4. 04遵循减少动态效果设置
  5. 05提供常见界面的动画模式

适用场景

  1. 01为按钮和链接增加交互反馈
  2. 02为卡片、弹窗添加进入动画
  3. 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–200msease-out按钮悬停、链接颜色、图标缩放
中等动效进入或离开视口的 UI 元素250–400mscubic-bezier(0,0,0.2,1)卡片显现、侧边栏滑入、模态框打开
宏观动效整个页面或区块的过渡400–600msease-in-out路由过渡、Hero 区块、引导流程

**经验法则:**不确定时,使用微动效。过度动画比没有动画更糟。

第 1 步:检查组件

阅读生成的组件文件。对每个组件,识别:

  1. 需要微动效的交互元素(按钮、链接、输入框、切换开关、带有 onClick 的卡片)
  2. 适合中等动效的显现元素(页面区块、卡片网格、侧边栏、模态框、抽屉、Toast)
  3. 适合宏观动效的 Hero 或标志性元素(主标题、特色图片、页面级过渡)

只为有明确用途的元素添加动画。如果无法用一句话解释某个元素为什么要动,就不要为它添加动画。

第 2 步:检测框架并选择动画方案

阅读 package.json 以确定框架,然后使用相应方案:

框架方案
Next.js / ReactCSS + 可选的 Framer Motion
SvelteKit / SvelteSvelte 内置过渡 + 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 步:为现有组件添加动画

修改现有文件时:

  1. 先阅读每个组件文件——了解当前结构
  2. 仅为 Micro 层级添加 CSS 类(绝不要更改 Micro 的组件逻辑)
  3. 使用 motion 组件包裹 Meso/Macro 层级(React),或添加 transition 指令(Svelte)
  4. 如果主 CSS 文件中尚未添加 reduced-motion 覆盖规则,则添加该规则
  5. 测试两种状态——启用和禁用动画(使用浏览器 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 组件的即用型模式目录,可直接复制粘贴

评论

支持 Markdown