依据WCAG 2.2设计和检查Web、iOS及Android界面,核查读屏标签、键盘操作、配色对比度和操作目标尺寸,使界面支持辅助技术。
- 为纯图标按钮添加说明
- 检查键盘操作与模态框焦点
- 检查对比度与操作目标尺寸
为React / Next.js界面的按钮、弹窗、通知和页面切换添加动画,实现元素显示、消失及随滚动触发的动态效果。
Production-ready animation patterns for React / Next.js — button, modal, toast, stagger, page transitions, exit animations, scroll, and layout — built on motion-foundations tokens and springs. Use when animating a specific UI element in React or Next.js — button, modal, toast, stagger, page transition, or scroll.
motion-patterns为React / Next.js界面实现响应用户操作和显示变化的动画。涵盖按钮反馈、弹窗和通知的显示与消失、列表项依次出现,以及页面切换。还支持与滚动联动的进度条、卡片展开,以及图像在不同显示位置之间的切换动画。
适合希望为界面中的各个组件添加动态效果的开发者,也适合需要同时完善元素出现和消失效果的情况。实现方案还考虑了弹窗的键盘操作,以及减少动态效果的设置。
本技能使用motion/react,需要先准备定义共用动画设置的motion-foundations。弹窗需要实现焦点管理、按Escape键关闭等功能。拖动交互、文字或SVG动画,以及仅使用CSS实现的过渡效果不在支持范围内。
本介绍由 AI(OpenAI Codex)根据公开的 SKILL.md 撰写。准确的规格请查看下方原文。
AI 仅根据 SKILL.md 的内容,从 4 个方面进行了评估。星标数和热度不计入评估。
总分按每项 A=25 分、B=15 分、C=5 分相加。 由 AI(OpenAI Codex)于 2026年10月19日 评估。仅供参考,安装前请同时查看原文。
安装前可以查看交给 AI 代理的指令内容。
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.
This skill produces:
AnimatePresence-wrapped conditional renders with correct exit behavioruseScroll + useTransformlayout, layoutId) for expanding and crossfading elementsmotion-foundations. No raw numbers.AnimatePresence with a key.layout is used only for small, isolated shifts. Large subtrees get explicit transforms.AnimatePresence with a key on the direct child. Without a key, exit animations never fire.exit when defining initial + animate. An animation without an exit is incomplete.mode="wait" on page transitions. Enter must not start until exit completes.layout on subtrees with more than ~5 children or deeply nested DOM. Use explicit x/y transforms instead.0.05s and 0.10s. Below feels mechanical; above feels sluggish.role="dialog", aria-modal="true".viewport={{ once: true }}. Repeating on scroll-out is distracting, not informative.motion-foundations. No inline numbers.| Situation | Pattern |
|---|---|
| Element appears / disappears | AnimatePresence |
| List of items loading in sequence | Stagger variants |
| Navigating between routes | Page 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 |
mode="wait" vs mode="sync"| Mode | Use 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 |
Three things must always be true:
AnimatePresence wraps the conditionalkeyexit propMiss any one of these and the exit animation silently fails.
layout — animates the element's own size/position change in placelayoutId — links two separate elements, crossfading between them across rendersUse layout="position" on text inside an expanding container to prevent text reflow from animating.
"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, // 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>
"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}
/>
</>
)
}
"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>
// 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 — rule 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" 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>
)
}
// 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" />
<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>
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:
"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>
)
}
This skill does not cover:
motion-foundationsmotion-advancedmotion-advancedmotion-advancedmotion-advancedmotion/react| Anti-pattern | Rule violated | Fix |
|---|---|---|
AnimatePresence child missing key | Rule 1 | Add stable key to the direct child |
initial + animate without exit | Rule 2 | Always define all three together |
Page transition without mode="wait" | Rule 3 | Add mode="wait" to AnimatePresence |
layout on a 50-item list | Rule 4 | Use mode="popLayout" or explicit transforms |
staggerChildren: 0.2 on a 10-item list | Rule 5 | Cap at 0.08–0.10 |
| Modal without focus trap | Rule 6 | Add focus-trap-react or Radix Dialog |
whileInView without viewport={{ once: true }} | Rule 7 | Repeating entrances distract, not inform |
transition={{ duration: 0.3 }} inline | Rule 8 | Use motionTokens.duration.normal |
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.还没有评价。欢迎分享你的使用感受。
无需登录即可发表。欢迎分享使用感受、提问,或说明在哪些环境下无法运行。
还没有评论。来写第一条吧。
依据WCAG 2.2设计和检查Web、iOS及Android界面,核查读屏标签、键盘操作、配色对比度和操作目标尺寸,使界面支持辅助技术。
从指令、记忆、工具执行和界面显示等十二个层面排查人工智能代理故障,依据代码和日志梳理原因,按严重程度列出问题并提出修复方案。
用实际开发任务比较多个编程智能体,记录成功率、可获取的API费用、耗时和重复运行的稳定性,为工具选型及更新后的性能评估提供数据。
设计和调整AI智能体使用的工具、输入与返回格式,明确出错后的恢复、重试和停止条件,管理上下文信息量,并以完成率、重试次数和成功任务成本评估改进。
根据错误信息和执行环境,分析AI代理反复失败或重复操作的原因。通过小范围、可撤销的恢复操作检验假设,并将结果与依据整理成可供交接的报告。
在AI代理反复失败或重复操作时,记录错误与环境前提、排查原因,采取影响有限的恢复操作,并汇总结果和证据,为有依据的重试与交接提供支持。