motion-patterns

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Motion Patterns

Motion 动画模式

Copy-paste patterns for the most common UI animation needs. Every pattern here is built on
motion-foundations
tokens and springs. Do not define new duration or easing values here — import them.
以下是可直接复制粘贴的、满足最常见UI动画需求的模式。 这里的每个模式都基于
motion-foundations
的tokens和springs构建。 请勿在此处定义新的时长或缓动值——请直接导入使用。

When to Activate

适用场景

  • Animating a button, card, modal, or toast notification
  • Building list entrances with stagger
  • Setting up page transitions in Next.js App Router
  • Adding entrance or exit animations to conditional content
  • Implementing scroll-reveal, scroll-linked progress, or sticky story sections
  • Building expanding cards, accordions, or shared-element transitions
  • 为按钮、卡片、模态框或提示通知添加动画
  • 构建带有序列效果的列表入场动画
  • 在Next.js App Router中设置页面过渡效果
  • 为条件渲染内容添加入场或退出动画
  • 实现滚动显示、滚动关联进度或粘性故事板块
  • 构建可展开卡片、折叠面板或共享元素过渡效果

Outputs

产出内容

This skill produces:
  • Accessible, SSR-safe animation for all standard UI components
  • AnimatePresence
    -wrapped conditional renders with correct exit behavior
  • Page transition wrapper component for Next.js App Router
  • Scroll-reveal and scroll-linked patterns using
    useScroll
    +
    useTransform
  • Layout animation patterns (
    layout
    ,
    layoutId
    ) for expanding and crossfading elements
本技能可生成:
  • 适用于所有标准UI组件的无障碍、支持SSR的动画
  • 包裹
    AnimatePresence
    的条件渲染组件,具备正确的退出行为
  • 用于Next.js App Router的页面过渡包装组件
  • 使用
    useScroll
    +
    useTransform
    实现的滚动显示和滚动关联模式
  • 用于元素展开和淡入淡出的布局动画模式(
    layout
    layoutId

Principles

设计原则

  • Every pattern imports from
    motion-foundations
    . No raw numbers.
  • Every conditional render is wrapped in
    AnimatePresence
    with a
    key
    .
  • Exit animations are always defined alongside enter animations — never as an afterthought.
  • layout
    is used only for small, isolated shifts. Large subtrees get explicit transforms.
  • 每个模式都从
    motion-foundations
    导入配置,不使用原始数值。
  • 每个条件渲染都用
    AnimatePresence
    包裹,并为直接子元素设置
    key
  • 退出动画始终与入场动画一同定义——绝不事后补充。
  • layout
    仅用于小型、独立的位置偏移。大型子树需使用显式变换。

Rules

规则要求

  1. Always wrap conditional renders in
    AnimatePresence
    with a
    key
    on the direct child. Without a key, exit animations never fire.
  2. Always define
    exit
    when defining
    initial
    +
    animate
    .
    An animation without an exit is incomplete.
  3. Use
    mode="wait"
    on page transitions.
    Enter must not start until exit completes.
  4. Never use
    layout
    on subtrees with more than ~5 children or deeply nested DOM.
    Use explicit
    x
    /
    y
    transforms instead.
  5. Stagger interval must stay between
    0.05s
    and
    0.10s
    .
    Below feels mechanical; above feels sluggish.
  6. Modals must always include: focus trap, Escape-key close, scroll lock,
    role="dialog"
    ,
    aria-modal="true"
    .
  7. Scroll reveals use
    viewport={{ once: true }}
    .
    Repeating on scroll-out is distracting, not informative.
  8. All token values are imported from
    motion-foundations
    .
    No inline numbers.
  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
    导入
    。不使用内联数值。

Decision Guidance

决策指南

Choosing the right pattern

选择合适的模式

SituationPattern
Element appears / disappears
AnimatePresence
List of items loading in sequenceStagger variants
Navigating between routesPage transition wrapper
Element changes size in place
layout
prop
Same element moves across page contexts
layoutId
Element enters when scrolled into view
whileInView
Value tied to scroll position
useScroll
+
useTransform
场景模式
元素出现/消失
AnimatePresence
列表项按顺序加载序列变体(Stagger variants)
路由间导航页面过渡包装组件
元素在原位改变尺寸
layout
属性
同一元素在不同页面上下文间移动
layoutId
元素滚动进入视图时触发入场
whileInView
值与滚动位置关联
useScroll
+
useTransform

When to use
mode="wait"
vs
mode="sync"

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

ModeUse when
wait
Page transitions, content swaps (one at a time)
sync
Stacked notifications, list items (overlap is fine)
popLayout
Items removed from a reflow list
模式适用场景
wait
页面过渡、内容切换(一次仅显示一个)
sync
堆叠通知、列表项(允许重叠)
popLayout
从回流列表中移除项时

Core Concepts

核心概念

AnimatePresence contract

AnimatePresence 约定

Three things must always be true:
  1. AnimatePresence
    wraps the conditional
  2. The direct child has a
    key
  3. The child has an
    exit
    prop
Miss any one of these and the exit animation silently fails.
必须同时满足以下三点:
  1. AnimatePresence
    包裹条件渲染
  2. 直接子元素拥有
    key
  3. 子元素拥有
    exit
    属性
缺少任何一点,退出动画都会静默失效。

layout vs layoutId

layout vs layoutId

  • layout
    — animates the element's own size/position change in place
  • layoutId
    — links two separate elements, crossfading between them across renders
Use
layout="position"
on text inside an expanding container to prevent text reflow from animating.
  • layout
    —— 原地动画元素自身的尺寸/位置变化
  • layoutId
    —— 关联两个独立元素,在渲染间实现淡入淡出过渡
在可展开容器内的文本上使用
layout="position"
,可防止文本重排被动画化。

Code Examples

代码示例

Button feedback

按钮反馈

tsx
"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}
/>
tsx
"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}
/>

Stagger list

序列列表

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

const container = {
  hidden: {},
  visible: {
    transition: {
      staggerChildren: 0.08,   // within the 0.05–0.10 rule
      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>
tsx
"use client"
import { motion } from "motion/react"
import { motionTokens, springs } from "@/lib/motion-tokens"

const container = {
  hidden: {},
  visible: {
    transition: {
      staggerChildren: 0.08,   // within the 0.05–0.10 rule
      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>

Modal

模态框

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

// Wrap at the call site:
// <AnimatePresence>{isOpen && <Modal key="modal" />}</AnimatePresence>

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

      {/* Panel — accessibility requirements: focus trap, Escape close,
          scroll lock, 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}
      />
    </>
  )
}
tsx
"use client"
import { motion, AnimatePresence } from "motion/react"
import { motionTokens, springs } from "@/lib/motion-tokens"

// Wrap at the call site:
// <AnimatePresence>{isOpen && <Modal key="modal" />}</AnimatePresence>

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

      {/* Panel — accessibility requirements: focus trap, Escape close,
          scroll lock, 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 stack

提示框堆叠

tsx
"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>
tsx
"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>

Page transition (Next.js App Router)

页面过渡(Next.js App Router)

tsx
// 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>
  )
}
tsx
// 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>
  )
}

Scroll reveal

滚动显示

tsx
"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 — rule 7
  transition={{ duration: motionTokens.duration.slow, ease: motionTokens.easing.smooth }}
/>
tsx
"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 — rule 7
  transition={{ duration: motionTokens.duration.slow, ease: motionTokens.easing.smooth }}
/>

Scroll progress bar

滚动进度条

tsx
"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 }}
    />
  )
}
tsx
"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 }}
    />
  )
}

Expanding card

可展开卡片

tsx
"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" prevents text reflow from animating */}
      <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>
  )
}
tsx
"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" prevents text reflow from animating */}
      <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>
  )
}

Shared-element crossfade

共享元素淡入淡出

tsx
// Source context
<motion.img layoutId="hero-image" src={src} className="w-16 h-16 rounded" />

// Destination context (same layoutId — motion handles the transition)
<motion.img layoutId="hero-image" src={src} className="w-full rounded-xl" />
tsx
// Source context
<motion.img layoutId="hero-image" src={src} className="w-16 h-16 rounded" />

// Destination context (same layoutId — motion handles the transition)
<motion.img layoutId="hero-image" src={src} className="w-full rounded-xl" />

Accordion

折叠面板

tsx
<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>
tsx
<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>

End-to-End Example

端到端示例

A staggered list that enters on mount, handles conditional presence, and respects reduced motion — combining tokens, springs, AnimatePresence, and the accessibility hook from
motion-foundations
:
tsx
"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>
  )
}
一个在挂载时入场、支持条件渲染、并遵循减少动画偏好的序列列表——结合了tokens、springs、AnimatePresence和
motion-foundations
中的无障碍钩子:
tsx
"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>
  )
}

Constraints / Non-Goals

约束/非目标

This skill does not cover:
  • Token and spring definitions → see
    motion-foundations
  • Drag interactions, swipe gestures, reorderable lists → see
    motion-advanced
  • Text animations (word/character reveal, counters) → see
    motion-advanced
  • SVG path drawing or morphing → see
    motion-advanced
  • Custom animation hooks → see
    motion-advanced
  • CSS-only transitions not using
    motion/react
本技能涵盖以下内容:
  • Token和spring的定义 → 请查看
    motion-foundations
  • 拖拽交互、滑动手势、可重新排序列表 → 请查看
    motion-advanced
  • 文本动画(单词/字符显示、计数器) → 请查看
    motion-advanced
  • SVG路径绘制或变形 → 请查看
    motion-advanced
  • 自定义动画钩子 → 请查看
    motion-advanced
  • 不使用
    motion/react
    的纯CSS过渡

Anti-Patterns

反模式

Anti-patternRule violatedFix
AnimatePresence
child missing
key
Rule 1Add stable
key
to the direct child
initial
+
animate
without
exit
Rule 2Always define all three together
Page transition without
mode="wait"
Rule 3Add
mode="wait"
to
AnimatePresence
layout
on a 50-item list
Rule 4Use
mode="popLayout"
or explicit transforms
staggerChildren: 0.2
on a 10-item list
Rule 5Cap at
0.08–0.10
Modal without focus trapRule 6Add
focus-trap-react
or Radix Dialog
whileInView
without
viewport={{ once: true }}
Rule 7Repeating entrances distract, not inform
transition={{ duration: 0.3 }}
inline
Rule 8Use
motionTokens.duration.normal
反模式违反规则修复方案
AnimatePresence
的子元素缺少
key
规则1为直接子元素添加稳定的
key
仅定义
initial
+
animate
,未定义
exit
规则2始终同时定义三者
页面过渡未使用
mode="wait"
规则3
AnimatePresence
添加
mode="wait"
在包含50个项的列表上使用
layout
规则4使用
mode="popLayout"
或显式变换
10项列表使用
staggerChildren: 0.2
规则5将值限制在
0.08–0.10
之间
模态框缺少焦点陷阱规则6添加
focus-trap-react
或Radix Dialog
whileInView
未设置
viewport={{ once: true }}
规则7重复入场动画会分散注意力,无实际价值
内联设置
transition={{ duration: 0.3 }}
规则8使用
motionTokens.duration.normal

Related Skills

相关技能

  • motion-foundations
    — defines all tokens, springs, the
    useSafeMotion
    hook, and SSR guards that every pattern here imports. Must be set up first.
  • motion-advanced
    — extends these patterns with drag, gestures, SVG, text, custom hooks, and imperative sequencing. Does not redefine any patterns from this skill.
  • motion-foundations
    —— 定义了本技能中所有模式导入的tokens、springs、
    useSafeMotion
    钩子和SSR防护。必须先完成该技能的配置。
  • motion-advanced
    —— 在本技能的基础上扩展了拖拽、手势、SVG、文本、自定义钩子和命令式序列功能。不会重定义本技能中的任何模式。