micro-interaction
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseMicro-interactions (UI Motion)
微交互(UI动效)
Small, functional motion that makes an interface feel responsive and alive: hover/press/focus feedback, toggles, toasts, drawers, and list/layout animation. The goal is feedback and continuity, not decoration.
这类小型功能性动效让界面更具响应感和鲜活感:包括悬停/按压/聚焦反馈、切换控件、提示框、抽屉以及列表/布局动画。核心目的是提供反馈和视觉连贯性,而非装饰。
When to use
适用场景
- Hover/press/focus feedback; toggles, checkboxes, like/heart buttons
- Toasts/snackbars, drawers, modals, tooltips, accordions (enter/exit)
- List add/remove/reorder; shared-element ("magic move") layout transitions
- Loading → success → error state transitions
- 悬停/按压/聚焦反馈;切换按钮、复选框、点赞/爱心按钮
- 提示框/消息条、抽屉、模态框、工具提示、折叠面板(入场/退场动画)
- 列表增删/排序;共享元素(「魔法移动」)布局过渡
- 加载→成功→错误状态过渡
Principles (apply to all of it)
设计原则(适用于所有动效)
- Duration: UI micro-interactions live in 100–250ms. Anything over ~400ms feels laggy for a click response.
- Animate and
transformonly — they are GPU-composited (no layout/paint). Avoid animatingopacity; usewidth/height/top/leftor layout animation instead.scale - Give instant press feedback: on tap with a fast spring.
scale: 0.96 - Asymmetric timing: enter slightly slower (ease-out), exit faster (ease-in). Things should arrive gracefully and leave promptly.
- Respect : gate non-essential motion; keep opacity changes, drop large movement.
prefers-reduced-motion - Easing defaults: ease-out for entrances ; a tasteful overshoot is
cubic-bezier(0.16, 1, 0.3, 1); standard movecubic-bezier(0.34, 1.56, 0.64, 1).cubic-bezier(0.4, 0, 0.2, 1)
- 时长:UI微交互的时长应控制在100–250ms。点击响应超过约400ms会让用户感觉卡顿。
- 仅动画和
transform属性——它们由GPU合成(不会触发布局/绘制)。避免对opacity做动画;改用width/height/top/left或布局动画替代。scale - 提供即时按压反馈:点击时应用并搭配快速弹簧效果。
scale: 0.96 - 非对称时序:入场稍慢(ease-out),退场更快(ease-in)。元素应优雅入场,迅速退场。
- 尊重:禁用非必要动效;保留透明度变化,移除大幅位移。
prefers-reduced-motion - 默认缓动函数:入场使用ease-out ;略带回弹效果使用
cubic-bezier(0.16, 1, 0.3, 1);标准移动使用cubic-bezier(0.34, 1.56, 0.64, 1)。cubic-bezier(0.4, 0, 0.2, 1)
Framer Motion (motion/react) essentials
Framer Motion(motion/react)核心用法
As of v11+, the package is imported as (the name still works).
motion/reactframer-motion从v11版本开始,该包需通过导入(名称仍可使用)。
motion/reactframer-motionGestures and springs
手势与弹簧效果
jsx
import { 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 }}
/>Spring intuition: higher = faster; higher = less bounce. is a snappy UI default. For visible bounce, lower damping (e.g. ).
stiffnessdampingstiffness: 400, damping: 30damping: 12jsx
import { 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 }}
/>弹簧参数说明:值越高,动画速度越快;值越高,回弹效果越弱。是适用于UI的快捷默认值。若需要明显回弹效果,降低damping值(例如)。
stiffnessdampingstiffness: 400, damping: 30damping: 12Enter / exit with AnimatePresence
使用AnimatePresence实现入场/退场动画
Exit animations require wrapping conditionally-rendered children, each with a stable .
AnimatePresencekeyjsx
import { 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>For toasts/lists, use so removed items don't hold space while exiting.
mode="popLayout"退场动画需要用包裹条件渲染的子元素,且每个子元素需设置唯一的。
AnimatePresencekeyjsx
import { 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 animation (the superpower)
布局动画(核心优势)
layoutjsx
<motion.li layout transition={{ type: "spring", stiffness: 500, damping: 40 }} />Shared-element transition across components: give two elements the same and Framer Motion animates between them as one mounts and the other unmounts.
layoutIdjsx
{!open && <motion.div layoutId="card" onClick={() => setOpen(true)} />}
<AnimatePresence>
{open && <motion.div layoutId="card" />} {/* "magic moves" from the source */}
</AnimatePresence>Gotcha: a plain element distorts and text during scale. Add to direct children that should counter-scale, and prefer / as motion values, or use to animate position only.
layoutborder-radiuslayoutborderRadiusboxShadowlayout="position"layoutjsx
<motion.li layout transition={{ type: "spring", stiffness: 500, damping: 40 }} />跨组件共享元素过渡:给两个元素设置相同的,Framer Motion会在一个元素挂载、另一个元素卸载时自动实现两者间的动画过渡。
layoutIdjsx
{!open && <motion.div layoutId="card" onClick={() => setOpen(true)} />}
<AnimatePresence>
{open && <motion.div layoutId="card" />} {/* 从源元素「魔法移动」过来 */}
</AnimatePresence>注意事项:单纯使用的元素在缩放时会扭曲和文本。需给需要抵消缩放的直接子元素添加属性,优先将/设为动效值,或使用仅动画位置变化。
layoutborder-radiuslayoutborderRadiusboxShadowlayout="position"Pure CSS path (no JS, including discrete properties)
纯CSS实现方案(无需JS,支持离散属性)
Modern CSS can animate enter/exit and even toggles without a library.
displaycss
.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-discretedisplayoverlayAlways include a reduced-motion guard:
css
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
transition-duration: 0.01ms !important;
}
}现代CSS无需依赖库即可实现入场/退场动画,甚至支持切换动画。
displaycss
.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务必添加减少动效的适配:
css
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
transition-duration: 0.01ms !important;
}
}State, not just decoration
动效服务于状态,而非仅装饰
Loading → success → error should be one continuous motion (e.g. a button morphs spinner → checkmark), not a jump cut. Keep the element mounted and animate between states so the eye tracks the same object.
加载→成功→错误状态应是连续的动效(例如按钮从加载 spinner 变形为对勾图标),而非生硬切换。保持元素挂载状态并在不同状态间做动画,让用户视线能追踪同一对象。
Deliver & verify (standalone HTML)
交付与验证(独立HTML文件)
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
For a self-contained interaction demo (toggle, like button, toast, drawer) the deliverable is one HTML file that opens directly in a browser. Pure-CSS interactions ship as-is; for a Framer Motion demo, load React + from CDN () into one inline module — no build step. One file is the right tier; don't reach for a bundler.
motionesm.shOutput contract:
- One file: your markup plus either CSS transitions/
.htmlor an inline@starting-styleimporting<script type="module">from CDN.motion - A way to land on the resolved end state for a screenshot — interactions are state-driven, not time-driven, so freeze the state rather than a clock.
Seek harness — pin a deterministic state. A micro-interaction's "frames" are its states (idle / hover / pressed / open). applies the target state on load so a screenshot captures it settled:
?state=openhtml
<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>Verify loop — render → set state → screenshot → check: open each meaningful state (, , ), screenshot, and check fidelity (press feedback reads instant, exit runs before unmount) plus artifacts ( distorting /text, toast holding space after exit, FOUC, jank). Any headless tool works:
?state=idle?state=hover?state=openlayoutborder-radiusbash
npx playwright screenshot --wait-for-timeout=400 "file://$PWD/demo.html?state=open" open.pngBefore you finish:
- Opens standalone — no console errors, CDN React/(if used) resolves.
motion - The (or controlled-prop) freeze lands a deterministic, settled state.
?state= - Screenshotted across states — idle / active / open — matches the brief, no artifacts.
- honored — large movement dropped, opacity/feedback kept.
prefers-reduced-motion - Easing is intentional — enter ease-out, exit ease-in, durations in the 100–250ms band.
封装工具(目录):scripts/可冻结scripts/seek-shot.sh anim.html 0 1.5 3测试环境并截取各个时刻的截图;?t=N可将截图拼接为一张预览图,方便快速查看。详见scripts/contact-sheet.sh sheet.png frame-*.png。scripts/README.md
对于独立的交互演示(切换按钮、点赞按钮、提示框、抽屉),交付物应为可直接在浏览器中打开的单个HTML文件。纯CSS交互可直接交付;若使用Framer Motion演示,需从CDN()加载React + 并写入内联模块——无需构建步骤。单个文件是最合适的交付形式,无需使用打包工具。
esm.shmotion输出规范:
- 单个文件:包含标记语言,以及CSS过渡/
.html或导入@starting-style的内联motion。<script type="module"> - 提供一种方式来定格最终状态以截图——交互是状态驱动而非时间驱动的,因此应冻结状态而非时间。
状态定格工具——固定确定性状态。微交互的「帧」即其不同状态(空闲/悬停/按压/打开)。参数可在页面加载时应用目标状态,以便截图捕获稳定的最终状态:
?state=openhtml
<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>验证流程——渲染→设置状态→截图→检查:打开每个关键状态(、、),截图并检查保真度(按压反馈即时、退场动画在卸载前执行)以及异常问题(导致/文本扭曲、提示框退场后仍占用空间、FOUC、卡顿)。任何无头测试工具均可实现:
?state=idle?state=hover?state=openlayoutborder-radiusbash
npx playwright screenshot --wait-for-timeout=400 "file://$PWD/demo.html?state=open" open.png交付前检查:
- 可独立打开——无控制台错误,CDN加载的React/(若使用)可正常解析。
motion - (或受控属性)可定格确定性的稳定状态。
?state= - 已对所有状态(空闲/激活/打开)截图——符合需求,无异常问题。
- 已适配——移除大幅位移,保留透明度/反馈效果。
prefers-reduced-motion - 缓动函数符合设计——入场ease-out,退场ease-in,时长在100–250ms区间。
Quick reference
速查表
| Need | Approach |
|---|---|
| Press feedback | |
| Enter/exit | |
| Reorder / resize | |
| Magic move | shared |
| Toast stack | |
| No-JS enter | |
| Animate to display:none | |
| Accessibility | |
| 需求 | 实现方案 |
|---|---|
| 按压反馈 | |
| 入场/退场动画 | |
| 排序/尺寸变化 | |
| 魔法移动 | 共享 |
| 提示框堆叠 | |
| 无JS入场动画 | |
| 动画到display:none | |
| 无障碍适配 | 始终添加 |
Reference files
参考文件
- — variants + stagger,
references/framer-motion-recipes.mdmodes,AnimatePresence/layoutmagic move,layoutIddrag-to-sort, drag constraints, gesture composition, andReorder.useReducedMotion - —
references/css-recipes.md,@starting-style, popover/dialog exit animation, keyframe spinners/toggles,transition-behavior: allow-discretefor animatable custom properties, and@propertypatterns.prefers-reduced-motion
- —— 变体与 stagger、
references/framer-motion-recipes.md模式、AnimatePresence/layout魔法移动、layoutId拖拽排序、拖拽约束、手势组合以及Reorder。useReducedMotion - ——
references/css-recipes.md、@starting-style、弹出层/对话框退场动画、关键帧 spinner/切换按钮、transition-behavior: allow-discrete可动画自定义属性以及@property适配模式。prefers-reduced-motion