開発
#react
motion-patterns
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.
DeepseekModel
キュレーション済みスキル
品質 優秀 · 90
v1.0.0
取得
https://deepseekmodel.com/api/download.php?id=affaan-m-ecc-skills-motion-patterns-skill-md&format=skill
ダウンロード .skill
標準形式。system_prompt と model_config を収録し、任意の Agent で利用可能
.skill ファイルの system_prompt フィールドの実際の内容。
name motion-patterns description 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. tags ["motion","animation","ui-patterns"] category frontend author jeff metadata {"version":"1.0.0"} Motion Patterns 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. 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 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 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. Rules Always wrap conditional renders in AnimatePresence with a key on the direct child. Without a key, exit animations never fire. Always define exit when defining initial + animate . An animation without an exit is incomplete. Use mode="wait" on page transitions. Enter must not start until exit completes. Never use layout on subtrees with more than ~5 children or deeply nested DOM. Use explicit x / y transforms instead. Stagger interval must stay between 0.05s and 0.10s . Below feels mechanical; above feels sluggish. Modals must always include: focus trap, Escape-key close, scroll lock, role="dialog" , aria-modal="true" . Scroll reveals use viewport={{ once: true }} . Repeating on scroll-out is distracting, not informative. All token values are imported from motion-foundations . No inline numbers. Decision Guidance Choosing the right pattern 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 When to use 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 Core Concepts AnimatePresence contract Three things must always be true: AnimatePresence wraps the conditional The direct child has a key The child has an exit prop Miss any one of these and the exit animation silently fails. 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. Code Examples Button feedback "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 "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 "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 "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) // 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 "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 "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 "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 // 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 <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 : "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 > ) }
このスキルを起動するキーワード。クリックでコピーできます。
このスキルにはトリガーワードがありません。
ダウンロードした .skill に含まれるフィールド。
| フィールド | 説明 |
|---|---|
| format | フォーマット識別子(skill/v1) |
| skill_id | スキル固有 ID |
| name | スキル名 |
| version | バージョン |
| description | 説明 |
| category | カテゴリ(配列) |
| trigger_words | トリガーワード |
| tags | タグ |
| source | ソース |
| source_url | ソース URL(本ページ) |
| exported_at | エクスポート日時(ダウンロード毎) |
| system_prompt | システムプロンプト本文 |
| model_config | モデル設定:provider / model / temperature / max_tokens / top_p |
| examples | サンプル |
| install_guide | 各プラットフォームの導入説明(Coze / Dify / Claude / カスタム) |