Loading...
Loading...
Compare original and translation side by side
transformopacitywidth/height/top/leftscalescale: 0.96prefers-reduced-motioncubic-bezier(0.16, 1, 0.3, 1)cubic-bezier(0.34, 1.56, 0.64, 1)cubic-bezier(0.4, 0, 0.2, 1)transformopacitywidth/height/top/leftscalescale: 0.96prefers-reduced-motioncubic-bezier(0.16, 1, 0.3, 1)cubic-bezier(0.34, 1.56, 0.64, 1)cubic-bezier(0.4, 0, 0.2, 1)motion/reactframer-motionmotion/reactframer-motionimport { motion } from "motion/react";
<motion.button
whileHover={{ scale: 1.03 }}
whileTap={{ scale: 0.96 }}
whileFocus={{ boxShadow: "0 0 0 3px rgba(59,130,246,.5)" }}
transition={{ type: "spring", stiffness: 400, damping: 30 }}
/>stiffnessdampingstiffness: 400, damping: 30damping: 12import { motion } from "motion/react";
<motion.button
whileHover={{ scale: 1.03 }}
whileTap={{ scale: 0.96 }}
whileFocus={{ boxShadow: "0 0 0 3px rgba(59,130,246,.5)" }}
transition={{ type: "spring", stiffness: 400, damping: 30 }}
/>stiffnessdampingstiffness: 400, damping: 30damping: 12AnimatePresencekeyimport { AnimatePresence, motion } from "motion/react";
<AnimatePresence>
{open && (
<motion.div
key="panel"
initial={{ opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 8 }}
transition={{ duration: 0.2, ease: [0.16, 1, 0.3, 1] }}
/>
)}
</AnimatePresence>mode="popLayout"AnimatePresencekeyimport { AnimatePresence, motion } from "motion/react";
<AnimatePresence>
{open && (
<motion.div
key="panel"
initial={{ opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 8 }}
transition={{ duration: 0.2, ease: [0.16, 1, 0.3, 1] }}
/>
)}
</AnimatePresence>mode="popLayout"layout<motion.li layout transition={{ type: "spring", stiffness: 500, damping: 40 }} />layoutId{!open && <motion.div layoutId="card" onClick={() => setOpen(true)} />}
<AnimatePresence>
{open && <motion.div layoutId="card" />} {/* "magic moves" from the source */}
</AnimatePresence>layoutborder-radiuslayoutborderRadiusboxShadowlayout="position"layout<motion.li layout transition={{ type: "spring", stiffness: 500, damping: 40 }} />layoutId{!open && <motion.div layoutId="card" onClick={() => setOpen(true)} />}
<AnimatePresence>
{open && <motion.div layoutId="card" />} {/* 从源元素「魔法移动」过来 */}
</AnimatePresence>layoutborder-radiuslayoutborderRadiusboxShadowlayout="position"display.toast {
transition: opacity 0.2s ease, transform 0.2s ease;
/* allow animating to/from display:none and from initial render */
transition-behavior: allow-discrete;
}
.toast[hidden] { display: none; }
/* animate FROM these values on first render / when entering */
@starting-style {
.toast { opacity: 0; transform: translateY(8px); }
}@starting-styletransition-behavior: allow-discretedisplayoverlay@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
transition-duration: 0.01ms !important;
}
}display.toast {
transition: opacity 0.2s ease, transform 0.2s ease;
/* 允许在display:none和初始渲染时触发动画 */
transition-behavior: allow-discrete;
}
.toast[hidden] { display: none; }
/* 定义首次渲染/入场时的初始动画值 */
@starting-style {
.toast { opacity: 0; transform: translateY(8px); }
}@starting-styletransition-behavior: allow-discretedisplayoverlay@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
transition-duration: 0.01ms !important;
}
}Packaged helper ():scripts/freezes thescripts/seek-shot.sh anim.html 0 1.5 3harness and screenshots each moment;?t=Ntiles them for one-glance review. Seescripts/contact-sheet.sh sheet.png frame-*.png.scripts/README.md
motionesm.sh.html@starting-style<script type="module">motion?state=open<script>
const s = new URLSearchParams(location.search).get("state");
if (s) document.documentElement.dataset.state = s; // CSS keys off [data-state="open"]
// Framer Motion: set the controlled prop from `s` (e.g. const [open]=useState(s==="open"))
// For a CSS @keyframes loop instead, freeze it: el.style.animationDelay=(-N)+"s"; el.style.animationPlayState="paused";
window.__ready = true;
</script>?state=idle?state=hover?state=openlayoutborder-radiusnpx playwright screenshot --wait-for-timeout=400 "file://$PWD/demo.html?state=open" open.pngmotion?state=prefers-reduced-motion封装工具(目录):scripts/可冻结scripts/seek-shot.sh anim.html 0 1.5 3测试环境并截取各个时刻的截图;?t=N可将截图拼接为一张预览图,方便快速查看。详见scripts/contact-sheet.sh sheet.png frame-*.png。scripts/README.md
esm.shmotion.html@starting-stylemotion<script type="module">?state=open<script>
const s = new URLSearchParams(location.search).get("state");
if (s) document.documentElement.dataset.state = s; // CSS通过[data-state="open"]匹配状态
// Framer Motion:从`s`设置受控属性(例如const [open]=useState(s==="open"))
// 若使用CSS @keyframes循环动画,可冻结:el.style.animationDelay=(-N)+"s"; el.style.animationPlayState="paused";
window.__ready = true;
</script>?state=idle?state=hover?state=openlayoutborder-radiusnpx playwright screenshot --wait-for-timeout=400 "file://$PWD/demo.html?state=open" open.pngmotion?state=prefers-reduced-motion| Need | Approach |
|---|---|
| Press feedback | |
| Enter/exit | |
| Reorder / resize | |
| Magic move | shared |
| Toast stack | |
| No-JS enter | |
| Animate to display:none | |
| Accessibility | |
| 需求 | 实现方案 |
|---|---|
| 按压反馈 | |
| 入场/退场动画 | |
| 排序/尺寸变化 | |
| 魔法移动 | 共享 |
| 提示框堆叠 | |
| 无JS入场动画 | |
| 动画到display:none | |
| 无障碍适配 | 始终添加 |
references/framer-motion-recipes.mdAnimatePresencelayoutlayoutIdReorderuseReducedMotionreferences/css-recipes.md@starting-styletransition-behavior: allow-discrete@propertyprefers-reduced-motionreferences/framer-motion-recipes.mdAnimatePresencelayoutlayoutIdReorderuseReducedMotionreferences/css-recipes.md@starting-styletransition-behavior: allow-discrete@propertyprefers-reduced-motion