Communitygithub.com

motion-patterns

面向 React / Next.js 的生产级动画模式——按钮、弹窗、通知、列表交错、页面过渡、退出动画、滚动效果和布局动画。基于 motion-foundations 的 tokens 和 springs 构建。适用于在 React 或 Next.js 中为特定 UI 元素添加动画的场景。

motion-patterns 是什么?

motion-patterns 是一个专注于 React/Next.js 动画的技能,提供了可直接复制粘贴的 UI 动画模式集。它覆盖了最常见的动画需求,包括按钮反馈、弹窗、通知堆叠、列表交错入场、Next.js 页面过渡、滚动揭示、进度条、可展开卡片、共享元素过渡和手风琴效果。所有模式都严格基于 motion-foundations 的 tokens 和 springs 构建,禁止直接定义持续时间和缓动值。该技能涵盖了 AnimatePresence 的正确使用(包括 key 和 exit 的定义)、页面过渡的 mode 选择、layout 与 layoutId 的区别,以及无障碍性要求(如弹窗的焦点陷阱、ESC 关闭、角色属性)。脚本中提供了 10+ 个可直接运行的代码示例,并附有清晰的规则(8 条)和反模式对照表,帮助开发者避免常见错误。

兼容平台~Claude Code~Codex CLI✓Cursor
npx skills add https://github.com/affaan-m/ECC/tree/main/skills/motion-patterns

在你喜欢的 AI 中提问

打开一个已预加载此 Agent Skill 的新对话。

文档

Motion Patterns

针对最常见 UI 动画需求的可复制粘贴模式。 此处的每一个模式都基于 motion-foundations 的 tokens 和 springs 构建。请勿定义新的持续时间或缓动值——直接从 motion-foundations 导入。

何时激活

  • 为按钮、卡片、弹窗或 toast 通知添加动画
  • 构建带有交错效果的列表入场
  • 设置 Next.js App Router 中的页面过渡
  • 为条件渲染内容添加入场或出场动画
  • 实现滚动揭示、滚动关联进度或粘性区块故事
  • 构建可展开卡片、手风琴或共享元素过渡

输出

本技能产出:

  • 符合无障碍要求、SSR 安全的通用 UI 组件动画
  • 使用 AnimatePresence 包装的条件渲染,并包含正确的退出行为
  • 适用于 Next.js App Router 的页面过渡包装组件
  • 基于 useScroll + useTransform 的滚动揭示和滚动关联模式
  • 使用 layout 和 layoutId 实现的布局动画模式(展开、淡入淡出等)

原则

  • 每个模式都从 motion-foundations 导入值。不出现原生数字。
  • 每个条件渲染都使用 AnimatePresence 包裹,并带有一个 key。
  • 退出动画始终与入场动画同时定义——绝不是事后补充。
  • layout 仅用于小范围、孤立的尺寸变化。大型子树使用显式变换。

规则

  1. 始终使用 AnimatePresence 包裹条件渲染,并在直接子元素上设置 key。没有 key,退出动画永远不会触发。
  2. 定义 initial + animate 的同时必须定义 exit。没有退出动画的动画是不完整的。
  3. 在页面过渡中使用 mode="wait"。入场必须等到退出完成后才开始。
  4. 不要在有超过约 5 个子元素或深度嵌套的 DOM 子树上使用 layout。请使用显式的 x/y 变换。
  5. 交错间隔必须保持在 0.05s 到 0.10s 之间。低于此值会显得机械;高于此值会显得迟钝。
  6. 弹窗必须始终包含: 焦点陷阱、ESC 键关闭、滚动锁定、role="dialog"、aria-modal="true"。
  7. 滚动揭示使用 viewport={{ once: true }}。在滚动出时重复播放会分散注意力,而不是提供信息。
  8. 所有 token 值都从 motion-foundations 导入。无内联数字。

决策指南

选择正确的模式

情况模式
元素显示 / 隐藏AnimatePresence
列表项依次加载Stagger variants
在路由之间导航页面过渡包装组件
元素在原地改变尺寸layout prop
同一元素在不同页面上下文中移动layoutId
元素在滚动进入视口时显示whileInView
与滚动位置关联的值useScroll + useTransform

何时使用 mode="wait" vs mode="sync"

模式使用场景
wait页面过渡、内容切换(一次一个)
sync堆叠通知、列表项(重叠是可以的)
popLayout从重新排列的列表中移除项目

核心概念

AnimatePresence 契约

三个条件必须始终满足:

  1. AnimatePresence 包裹条件渲染
  2. 直接子元素带有 key
  3. 子元素具有 exit prop

缺少任何一项,退出动画都会静默失败。

layout vs layoutId

  • layout — 就地动画化元素自身的尺寸/位置变化
  • layoutId — 链接两个不同的元素,在渲染之间进行淡入淡出过渡

在可展开容器内的文本上使用 layout="position" 可以防止文本重排导致的动画不自然。

代码示例

按钮反馈

"use client"
import { motion } from "motion/react"
import { springs, motionTokens } from "@/lib/motion-tokens"

<motion.button
  whileHover={{ scale: motionTokens.scale.pop }}
  whileTap={{ scale: motionTokens.scale.press }}
  transition={springs.snappy}
/>

交错列表

"use client"
import { motion } from "motion/react"
import { motionTokens, springs } from "@/lib/motion-tokens"

const container = {
  hidden: {},
  visible: {
    transition: {
      staggerChildren: 0.08,   // 在 0.05–0.10 规则范围内
      delayChildren: 0.1,
    },
  },
}

const item = {
  hidden:  { opacity: 0, y: motionTokens.distance.md },
  visible: { opacity: 1, y: 0, transition: springs.gentle },
}

<motion.ul variants={container} initial="hidden" animate="visible">
  {items.map((i) => (
    <motion.li key={i.id} variants={item} />
  ))}
</motion.ul>

弹窗

"use client"
import { motion, AnimatePresence } from "motion/react"
import { motionTokens, springs } from "@/lib/motion-tokens"

// 在调用处包裹:
// <AnimatePresence>{isOpen && <Modal key="modal" />}</AnimatePresence>

export function Modal({ onClose }: { onClose: () => void }) {
  return (
    <>
      {/* 遮罩层 */}
      <motion.div
        className="fixed inset-0 bg-black/50"
        initial={{ opacity: 0 }}
        animate={{ opacity: 1 }}
        exit={{ opacity: 0 }}
        onClick={onClose}
      />

      {/* 面板 — 无障碍需求:焦点陷阱、ESC 关闭、
          滚动锁定、role="dialog"、aria-modal="true" */}
      <motion.div
        role="dialog"
        aria-modal="true"
        className="fixed inset-x-4 top-1/2 -translate-y-1/2 rounded-xl bg-white p-6"
        initial={{
          opacity: 0,
          scale: motionTokens.scale.press,
          y: motionTokens.distance.sm,
        }}
        animate={{ opacity: 1, scale: 1, y: 0 }}
        exit={{
          opacity: 0,
          scale: motionTokens.scale.press,
          y: motionTokens.distance.sm,
        }}
        transition={springs.gentle}
      />
    </>
  )
}

Toast 堆叠

"use client"
import { motion, AnimatePresence } from "motion/react"
import { motionTokens, springs } from "@/lib/motion-tokens"

<AnimatePresence mode="sync">
  {toasts.map((t) => (
    <motion.div
      key={t.id}
      layout
      initial={{
        opacity: 0,
        x: motionTokens.distance.xl,
        scale: motionTokens.scale.subtle,
      }}
      animate={{ opacity: 1, x: 0, scale: 1 }}
      exit={{
        opacity: 0,
        x: motionTokens.distance.xl,
        scale: motionTokens.scale.subtle,
      }}
      transition={springs.snappy}
    />
  ))}
</AnimatePresence>

页面过渡(Next.js App Router)

// components/page-transition.tsx
"use client"
import { motion, AnimatePresence } from "motion/react"
import { usePathname } from "next/navigation"
import { motionTokens } from "@/lib/motion-tokens"

const variants = {
  initial: { opacity: 0, y: motionTokens.distance.sm },
  enter:   { opacity: 1, y: 0 },
  exit:    { opacity: 0, y: -motionTokens.distance.sm },
}

export function PageTransition({ children }: { children: React.ReactNode }) {
  const pathname = usePathname()
  return (
    <AnimatePresence mode="wait">
      <motion.div
        key={pathname}
        variants={variants}
        initial="initial"
        animate="enter"
        exit="exit"
        transition={{
          duration: motionTokens.duration.normal,
          ease: motionTokens.easing.smooth,
        }}
      >
        {children}
      </motion.div>
    </AnimatePresence>
  )
}

滚动揭示

"use client"
import { motion } from "motion/react"
import { motionTokens, springs } from "@/lib/motion-tokens"

<motion.div
  initial={{ opacity: 0, y: motionTokens.distance.lg }}
  whileInView={{ opacity: 1, y: 0 }}
  viewport={{ once: true, margin: "-80px" }}   // once: true — 规则 7
  transition={{ duration: motionTokens.duration.slow, ease: motionTokens.easing.smooth }}
/>

滚动进度条

"use client"
import { motion, useScroll } from "motion/react"

export function ScrollProgress() {
  const { scrollYProgress } = useScroll()
  return (
    <motion.div
      className="fixed top-0 left-0 h-1 bg-indigo-500 origin-left w-full"
      style={{ scaleX: scrollYProgress }}
    />
  )
}

可展开卡片

"use client"
import { useState } from "react"
import { motion, AnimatePresence } from "motion/react"
import { springs, motionTokens } from "@/lib/motion-tokens"

export function ExpandingCard({ title, body }: { title: string; body: string }) {
  const [expanded, setExpanded] = useState(false)

  return (
    <motion.div layout onClick={() => setExpanded(!expanded)} className="cursor-pointer">
      {/* layout="position" 可防止文本重排产生动画 */}
      <motion.h2 layout="position" className="font-semibold">
        {title}
      </motion.h2>

      <AnimatePresence>
        {expanded && (
          <motion.p
            key="body"
            initial={{ opacity: 0 }}
            animate={{ opacity: 1 }}
            exit={{ opacity: 0 }}
            transition={{ duration: motionTokens.duration.fast }}
          >
            {body}
          </motion.p>
        )}
      </AnimatePresence>
    </motion.div>
  )
}

共享元素淡入淡出

// 源上下文
<motion.img layoutId="hero-image" src={src} className="w-16 h-16 rounded" />

// 目标上下文(相同的 layoutId — motion 处理过渡)
<motion.img layoutId="hero-image" src={src} className="w-full rounded-xl" />

手风琴

<motion.div
  initial={false}
  animate={{ opacity: open ? 1 : 0, scaleY: open ? 1 : 0 }}
  style={{ transformOrigin: "top", overflow: "hidden" }}
  transition={{
    duration: motionTokens.duration.normal,
    ease: motionTokens.easing.smooth,
  }}
> {children}
</motion.div>

端到端示例

一个在挂载时交错的列表,处理条件渲染,并尊重 reduced-motion(减少动画偏好)。结合了 tokens、springs、AnimatePresence 以及来自 motion-foundations 的无障碍 hook:

"use client"
import { useState } from "react"
import { motion, AnimatePresence } from "motion/react"
import { motionTokens, springs } from "@/lib/motion-tokens"
import { useSafeMotion } from "@/hooks/use-reduced-motion"

const containerVariants = {
  hidden: {},
  visible: {
    transition: { staggerChildren: 0.08, delayChildren: 0.1 },
  },
}

function ListItem({ label, onRemove }: { label: string; onRemove: () => void }) {
  const safe = useSafeMotion(motionTokens.distance.sm)
  return (
    <motion.li
      variants={{
        hidden:  safe.initial,
        visible: safe.animate,
      }}
      exit={safe.exit}
      transition={springs.gentle}
      className="flex items-center justify-between p-3 rounded-lg bg-white shadow-sm"
    >
      <span>{label}</span>
      <button onClick={onRemove}>Remove</button>
    </motion.li>
  )
}

export function AnimatedList({ items, onRemove }: {
  items: { id: string; label: string }[]
  onRemove: (id: string) => void
}) {
  return (
    <motion.ul
      variants={containerVariants}
      initial="hidden"
      animate="visible"
      className="space-y-2"
    >
      <AnimatePresence mode="popLayout">
        {items.map((item) => (
          <ListItem
            key={item.id}
            label={item.label}
            onRemove={() => onRemove(item.id)}
          />
        ))}
      </AnimatePresence>
    </motion.ul>
  )
}

约束 / 非目标

本技能不涵盖:

  • Token 和 spring 定义 → 请参阅 motion-foundations
  • 拖拽交互、滑动手势、可重新排序列表 → 请参阅 motion-advanced
  • 文本动画(单词/字符揭示、计数器) → 请参阅 motion-advanced
  • SVG 路径绘制或变形 → 请参阅 motion-advanced
  • 自定义动画 hooks → 请参阅 motion-advanced
  • 不使用 motion/react 的纯 CSS 过渡

反模式

反模式违反规则修复方法
AnimatePresence 子元素缺少 key规则 1为直接子元素添加稳定的 key
定义了 initial + animate 但缺少 exit规则 2始终同时定义三个属性
页面过渡未使用 mode="wait"规则 3在 AnimatePresence 中添加 mode="wait"
在有 50 个项目的列表上使用 layout规则 4使用 mode="popLayout" 或显式变换
staggerChildren: 0.2 应用于 10 个项目列表规则 5限制在 0.08–0.10
弹窗缺少焦点陷阱规则 6添加 focus-trap-react 或 Radix Dialog
whileInView 未使用 viewport={{ once: true }}规则 7重复入场会分散注意力而非提供信息
内联 transition={{ duration: 0.3 }}规则 8使用 motionTokens.duration.normal

相关技能

  • motion-foundations — 定义了所有 tokens、springs、useSafeMotion hook 以及此处每个模式都导入的 SSR 守卫。必须先设置好。
  • motion-advanced — 通过拖拽、手势、SVG、文本、自定义 hooks 和命令式排序扩展这些模式。不重新定义此技能中的任何模式。

Individual skills in this repo

This repo contains 20 individual skills — each has its own dedicated page.

accessibility

Design, implement, and audit inclusive digital products using WCAG 2.2 Level AA. Use when building or auditing UI that must meet WCAG 2.2 Level AA, or when reviewing a change for keyboard, contrast, or screen-reader support.

affaan-m/content-engine

Create platform-native content systems for X, LinkedIn, TikTok, YouTube, newsletters, and repurposed multi-platform campaigns. Use when the user wants social posts, threads, scripts, content calendars, or one source asset adapted cleanly across platforms.

affaan-m/ECC

Multi-platform content distribution across X, LinkedIn, Threads, and Bluesky. Adapts content per platform using content-engine patterns. Never posts identical content cross-platform. Use when the user wants to distribute content across social platforms.

affaan-m/fal-ai-media

Unified media generation via fal.ai MCP — image, video, and audio. Covers text-to-image (Nano Banana), text/image-to-video (Seedance, Kling, Veo 3), text-to-speech (CSM-1B), and video-to-audio (ThinkSound). Use when the user wants to generate images, videos, or audio with AI.

affaan-m/manim-video

日本語翻訳:このファイルは manim-video 用の日本語翻訳が必要です

affaan-m/remotion-video-creation

Remotion のベストプラクティス - React で動画を作成する。3D、アニメーション、音声、字幕、チャート、トランジションなどをカバーするドメイン固有の29のルール。

affaan-m/video-editing

AI-assisted video editing workflows for cutting, structuring, and augmenting real footage. Covers the full pipeline from raw capture through FFmpeg, Remotion, ElevenLabs, fal.ai, and final polish in Descript or CapCut. Use when the user wants to edit video, cut footage, create vlogs, or build video content.

agent-architecture-audit

Full-stack diagnostic for agent and LLM applications. Audits the 12-layer agent stack for wrapper regression, memory pollution, tool discipline failures, hidden repair loops, and rendering corruption. Produces severity-ranked findings with code-first fixes. Essential for developers building agent applications, autonomous loops, or any LLM-powered feature. Use when an agent or LLM feature misbehaves and the failing layer is unknown, or before shipping an agent stack.

agent-eval

Head-to-head comparison of coding agents (Claude Code, Aider, Codex, etc.) on custom tasks with pass rate, cost, time, and consistency metrics. Use when choosing between coding agents, or when a change to an agent setup needs measured pass rate, cost, and time rather than an impression.

agent-harness-construction

Design and optimize AI agent action spaces, tool definitions, and observation formatting for higher completion rates. Use when defining or revising an agent

agentic-engineering

Operate as an agentic engineer using eval-first execution, decomposition, and cost-aware model routing. Use when planning or executing engineering work that agents will carry out end to end.

agentic-os

Build persistent multi-agent operating systems on Claude Code. Covers kernel architecture, specialist agents, slash commands, file-based memory, scheduled automation, and state management without external databases. Use when building a persistent multi-agent system on Claude Code with its own memory, commands, and scheduling.

agent-introspection-debugging

Structured self-debugging workflow for AI agent failures using capture, diagnosis, contained recovery, and introspection reports. Use when an agent run fails and you need a reproducible diagnosis instead of a retry.

agent-payment-x402

Add x402 payment execution to AI agents with per-task budgets, spending controls, and non-custodial wallets. Supports Base through agentwallet-sdk and X Layer through OKX Payments / OKX Agent Payments Protocol. Use when an agent must pay for something itself and needs per-task budgets, spending controls, and a non-custodial wallet.

agent-self-evaluation

Use after completing any non-trivial task. The agent self-rates its output on 5 axes — accuracy, completeness, clarity, actionability, conciseness — with concrete evidence per criterion. Produces a structured 1-5 scorecard with specific improvement suggestions.

agent-sort

Build an evidence-backed ECC install plan for a specific repo by sorting skills, commands, rules, hooks, and extras into DAILY vs LIBRARY buckets using parallel repo-aware review passes. Use when ECC should be trimmed to what a project actually needs instead of loading the full bundle.

ai-first-engineering

Engineering operating model for teams where AI agents generate a large share of implementation output. Use when setting team process, review gates, or ownership rules for a codebase largely written by agents.

ai-regression-testing

Regression testing strategies for AI-assisted development. Sandbox-mode API testing without database dependencies, automated bug-check workflows, and patterns to catch AI blind spots where the same model writes and reviews code. Use when adding regression coverage to AI-assisted code, or when the same model both wrote and reviewed a change.

android-clean-architecture

Clean Architecture patterns for Android and Kotlin Multiplatform projects — module structure, dependency rules, UseCases, Repositories, and data layer patterns. Use when structuring modules, layers, or data flow in an Android or KMP project.

angular-developer

Generates Angular code and provides architectural guidance. Trigger when creating projects, components, or services, or for best practices on reactivity (signals, linkedSignal, resource), forms, dependency injection, routing, SSR, accessibility (ARIA), animations, styling (component styles, Tailwind CSS), testing, or CLI tooling.

相关技能