micro-interaction

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Micro-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
    transform
    and
    opacity
    only — they are GPU-composited (no layout/paint). Avoid animating
    width/height/top/left
    ; use
    scale
    or layout animation instead.
  • Give instant press feedback:
    scale: 0.96
    on tap with a fast spring.
  • Asymmetric timing: enter slightly slower (ease-out), exit faster (ease-in). Things should arrive gracefully and leave promptly.
  • Respect
    prefers-reduced-motion
    : gate non-essential motion; keep opacity changes, drop large movement.
  • Easing defaults: ease-out for entrances
    cubic-bezier(0.16, 1, 0.3, 1)
    ; a tasteful overshoot is
    cubic-bezier(0.34, 1.56, 0.64, 1)
    ; standard move
    cubic-bezier(0.4, 0, 0.2, 1)
    .
  • 时长:UI微交互的时长应控制在100–250ms。点击响应超过约400ms会让用户感觉卡顿。
  • 仅动画
    transform
    opacity
    属性——它们由GPU合成(不会触发布局/绘制)。避免对
    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
motion/react
(the
framer-motion
name still works).
从v11版本开始,该包需通过
motion/react
导入(
framer-motion
名称仍可使用)。

Gestures 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
stiffness
= faster; higher
damping
= less bounce.
stiffness: 400, damping: 30
is a snappy UI default. For visible bounce, lower damping (e.g.
damping: 12
).
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 }}
/>
弹簧参数说明:
stiffness
值越高,动画速度越快;
damping
值越高,回弹效果越弱。
stiffness: 400, damping: 30
是适用于UI的快捷默认值。若需要明显回弹效果,降低damping值(例如
damping: 12
)。

Enter / exit with AnimatePresence

使用AnimatePresence实现入场/退场动画

Exit animations require
AnimatePresence
wrapping conditionally-rendered children, each with a stable
key
.
jsx
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
mode="popLayout"
so removed items don't hold space while exiting.
退场动画需要用
AnimatePresence
包裹条件渲染的子元素,且每个子元素需设置唯一的
key
jsx
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)

布局动画(核心优势)

layout
automatically animates any layout change (position/size) using transforms — perfect for reordering, expanding cards, and grid<->list.
jsx
<motion.li layout transition={{ type: "spring", stiffness: 500, damping: 40 }} />
Shared-element transition across components: give two elements the same
layoutId
and Framer Motion animates between them as one mounts and the other unmounts.
jsx
{!open && <motion.div layoutId="card" onClick={() => setOpen(true)} />}
<AnimatePresence>
  {open && <motion.div layoutId="card" />} {/* "magic moves" from the source */}
</AnimatePresence>
Gotcha: a plain
layout
element distorts
border-radius
and text during scale. Add
layout
to direct children that should counter-scale, and prefer
borderRadius
/
boxShadow
as motion values, or use
layout="position"
to animate position only.
layout
属性会自动通过transform动画实现任何布局变化(位置/尺寸)——非常适合排序、卡片展开以及网格/列表切换场景。
jsx
<motion.li layout transition={{ type: "spring", stiffness: 500, damping: 40 }} />
跨组件共享元素过渡:给两个元素设置相同的
layoutId
,Framer Motion会在一个元素挂载、另一个元素卸载时自动实现两者间的动画过渡。
jsx
{!open && <motion.div layoutId="card" onClick={() => setOpen(true)} />}
<AnimatePresence>
  {open && <motion.div layoutId="card" />} {/* 从源元素「魔法移动」过来 */}
</AnimatePresence>
注意事项:单纯使用
layout
的元素在缩放时会扭曲
border-radius
和文本。需给需要抵消缩放的直接子元素添加
layout
属性,优先将
borderRadius
/
boxShadow
设为动效值,或使用
layout="position"
仅动画位置变化。

Pure CSS path (no JS, including discrete properties)

纯CSS实现方案(无需JS,支持离散属性)

Modern CSS can animate enter/exit and even
display
toggles without a library.
css
.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-style
defines the "before-open" values so an element animates in on first appearance;
transition-behavior: allow-discrete
lets
display
(and
overlay
for popovers/dialogs) participate so exit animations run before the element is removed. Baseline in Chrome/Edge/Safari; provide a no-animation fallback for older browsers.
Always include a reduced-motion guard:
css
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    transition-duration: 0.01ms !important;
  }
}
现代CSS无需依赖库即可实现入场/退场动画,甚至支持
display
切换动画。
css
.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-style
用于定义「打开前」的初始值,使元素首次出现时能播放入场动画;
transition-behavior: allow-discrete
允许
display
(以及弹出层/对话框的
overlay
属性)参与动画,确保退场动画在元素被移除前执行。该特性已在Chrome/Edge/Safari中支持;需为旧浏览器提供无动画降级方案。
务必添加减少动效的适配:
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/
):
scripts/seek-shot.sh anim.html 0 1.5 3
freezes the
?t=N
harness and screenshots each moment;
scripts/contact-sheet.sh sheet.png frame-*.png
tiles them for one-glance review. See
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 +
motion
from CDN (
esm.sh
) into one inline module — no build step. One file is the right tier; don't reach for a bundler.
Output contract:
  • One
    .html
    file: your markup plus either CSS transitions/
    @starting-style
    or an inline
    <script type="module">
    importing
    motion
    from CDN.
  • 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).
?state=open
applies the target state on load so a screenshot captures it settled:
html
<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 (
?state=idle
,
?state=hover
,
?state=open
), screenshot, and check fidelity (press feedback reads instant, exit runs before unmount) plus artifacts (
layout
distorting
border-radius
/text, toast holding space after exit, FOUC, jank). Any headless tool works:
bash
npx playwright screenshot --wait-for-timeout=400 "file://$PWD/demo.html?state=open" open.png
Before you finish:
  1. Opens standalone — no console errors, CDN React/
    motion
    (if used) resolves.
  2. The
    ?state=
    (or controlled-prop) freeze lands a deterministic, settled state.
  3. Screenshotted across states — idle / active / open — matches the brief, no artifacts.
  4. prefers-reduced-motion
    honored — large movement dropped, opacity/feedback kept.
  5. 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(
esm.sh
)加载React +
motion
并写入内联模块——无需构建步骤。单个文件是最合适的交付形式,无需使用打包工具。
输出规范:
  • 单个
    .html
    文件:包含标记语言,以及CSS过渡/
    @starting-style
    或导入
    motion
    的内联
    <script type="module">
  • 提供一种方式来定格最终状态以截图——交互是状态驱动而非时间驱动的,因此应冻结状态而非时间。
状态定格工具——固定确定性状态。微交互的「帧」即其不同状态(空闲/悬停/按压/打开)。
?state=open
参数可在页面加载时应用目标状态,以便截图捕获稳定的最终状态:
html
<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=open
),截图并检查保真度(按压反馈即时、退场动画在卸载前执行)以及异常问题
layout
导致
border-radius
/文本扭曲、提示框退场后仍占用空间、FOUC、卡顿)。任何无头测试工具均可实现:
bash
npx playwright screenshot --wait-for-timeout=400 "file://$PWD/demo.html?state=open" open.png
交付前检查:
  1. 可独立打开——无控制台错误,CDN加载的React/
    motion
    (若使用)可正常解析。
  2. ?state=
    (或受控属性)可定格确定性的稳定状态。
  3. 已对所有状态(空闲/激活/打开)截图——符合需求,无异常问题。
  4. 已适配
    prefers-reduced-motion
    ——移除大幅位移,保留透明度/反馈效果。
  5. 缓动函数符合设计——入场ease-out,退场ease-in,时长在100–250ms区间。

Quick reference

速查表

NeedApproach
Press feedback
whileTap={{ scale: 0.96 }}
spring 400/30
Enter/exit
AnimatePresence
+
initial/animate/exit
Reorder / resize
layout
prop (spring)
Magic moveshared
layoutId
Toast stack
AnimatePresence mode="popLayout"
No-JS enter
@starting-style
+ transition
Animate to display:none
transition-behavior: allow-discrete
Accessibility
prefers-reduced-motion
guard always
需求实现方案
按压反馈
whileTap={{ scale: 0.96 }}
搭配spring 400/30
入场/退场动画
AnimatePresence
+
initial/animate/exit
排序/尺寸变化
layout
属性(spring动画)
魔法移动共享
layoutId
提示框堆叠
AnimatePresence mode="popLayout"
无JS入场动画
@starting-style
+ transition
动画到display:none
transition-behavior: allow-discrete
无障碍适配始终添加
prefers-reduced-motion
适配

Reference files

参考文件

  • references/framer-motion-recipes.md
    — variants + stagger,
    AnimatePresence
    modes,
    layout
    /
    layoutId
    magic move,
    Reorder
    drag-to-sort, drag constraints, gesture composition, and
    useReducedMotion
    .
  • references/css-recipes.md
    @starting-style
    ,
    transition-behavior: allow-discrete
    , popover/dialog exit animation, keyframe spinners/toggles,
    @property
    for animatable custom properties, and
    prefers-reduced-motion
    patterns.
  • references/framer-motion-recipes.md
    —— 变体与 stagger、
    AnimatePresence
    模式、
    layout
    /
    layoutId
    魔法移动、
    Reorder
    拖拽排序、拖拽约束、手势组合以及
    useReducedMotion
  • references/css-recipes.md
    ——
    @starting-style
    transition-behavior: allow-discrete
    、弹出层/对话框退场动画、关键帧 spinner/切换按钮、
    @property
    可动画自定义属性以及
    prefers-reduced-motion
    适配模式。