to-spring-or-not-to-spring by raphaelsalaja/userinterface-wiki
npx skills add https://github.com/raphaelsalaja/userinterface-wiki --skill to-spring-or-not-to-spring根据交互类型审查动画代码,确保选择正确的时序函数。
文件:行号 格式输出检查结果| 优先级 | 类别 | 前缀 |
|---|---|---|
| 1 | 弹簧动画选择 | spring- |
| 2 | 缓动函数选择 | easing- |
| 3 | 持续时间 | duration- |
| 4 |
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
| 无动画 |
none- |
提问:这个动作是响应用户操作,还是系统在主动表达?
| 动作类型 | 最佳选择 | 原因 |
|---|---|---|
| 用户驱动(拖拽、轻拂、手势) | 弹簧动画 | 可被中断,保留速度 |
| 系统驱动(状态变化、反馈) | 缓动函数 | 明确的开始/结束,可预测的时序 |
| 时间表示(进度、加载) | 线性 | 时间与进度呈 1:1 关系 |
| 高频交互(打字、快速切换) | 无动画 | 动画会增加干扰,感觉更慢 |
spring-for-gestures手势驱动的动作(拖拽、轻拂、滑动)必须使用弹簧动画。
不通过:
<motion.div
drag="x"
transition={{ duration: 0.3, ease: "easeOut" }}
/>
通过:
<motion.div
drag="x"
transition={{ type: "spring", stiffness: 500, damping: 30 }}
/>
spring-for-interruptible可能被中断的动作必须使用弹簧动画。
不通过:
// 用户可以在动画中途再次点击
<motion.div
animate={{ x: isOpen ? 200 : 0 }}
transition={{ duration: 0.3 }}
/>
通过:
<motion.div
animate={{ x: isOpen ? 200 : 0 }}
transition={{ type: "spring", stiffness: 400, damping: 25 }}
/>
spring-preserves-velocity当速度很重要时,使用弹簧动画以保留输入能量。
不通过:
// 快速轻拂和慢速轻拂的动画效果相同
onDragEnd={(e, info) => {
animate(target, { x: 0 }, { duration: 0.3 });
}}
通过:
// 快速轻拂比慢速轻拂移动得更快
onDragEnd={(e, info) => {
animate(target, { x: 0 }, {
type: "spring",
velocity: info.velocity.x,
});
}}
spring-params-balanced弹簧参数必须平衡;避免过度振荡。
不通过:
transition={{
type: "spring",
stiffness: 1000,
damping: 5, // 太低 - 过度弹跳
}}
通过:
transition={{
type: "spring",
stiffness: 500,
damping: 30, // 平衡 - 快速稳定
}}
easing-for-state-change系统发起的状态变化应使用缓动曲线。
不通过:
// 使用弹簧动画的 Toast 通知
<motion.div
animate={{ y: 0 }}
transition={{ type: "spring" }}
/>
// 对于简单的通知来说感觉不安定
通过:
<motion.div
animate={{ y: 0 }}
transition={{ duration: 0.2, ease: "easeOut" }}
/>
easing-entrance-ease-out进入动画必须使用 ease-out(快速到达,轻柔稳定)。
不通过:
.modal-enter {
animation-timing-function: ease-in;
}
通过:
.modal-enter {
animation-timing-function: ease-out;
}
easing-exit-ease-in退出动画必须使用 ease-in(在离开前积累动量)。
不通过:
.modal-exit {
animation-timing-function: ease-out;
}
通过:
.modal-exit {
animation-timing-function: ease-in;
}
easing-transition-ease-in-out视图/模式转换使用 ease-in-out 以获得中性注意力。
通过:
.page-transition {
animation-timing-function: ease-in-out;
}
easing-linear-only-progress线性缓动仅用于进度条和时间表示。
不通过:
.card-slide {
transition: transform 200ms linear; /* 机械感 */
}
通过:
.progress-bar {
transition: width 100ms linear; /* 真实的时间表示 */
}
duration-press-hover按压和悬停交互:120-180ms。
不通过:
.button:hover {
transition: background-color 400ms;
}
通过:
.button:hover {
transition: background-color 150ms;
}
duration-small-state小的状态变化:180-260ms。
通过:
.toggle {
transition: transform 200ms ease;
}
duration-max-300ms用户发起的动画持续时间不得超过 300ms。
不通过:
<motion.div transition={{ duration: 0.5 }} />
通过:
<motion.div transition={{ duration: 0.25 }} />
duration-shorten-before-curve如果动画感觉缓慢,应先缩短持续时间,再调整曲线。
不通过(常见错误):
/* 试图通过更陡的曲线来修复缓慢问题 */
.element {
transition: 400ms cubic-bezier(0, 0.9, 0.1, 1);
}
通过:
/* 通过缩短持续时间来修复缓慢问题 */
.element {
transition: 200ms ease-out;
}
none-high-frequency高频交互不应有动画。
不通过:
// 每次按键都有动画
function SearchInput() {
return (
<motion.div animate={{ scale: [1, 1.02, 1] }}>
<input onChange={handleSearch} />
</motion.div>
);
}
通过:
function SearchInput() {
return <input onChange={handleSearch} />;
}
none-keyboard-navigation键盘导航应是即时的,无动画。
不通过:
function Menu() {
return items.map(item => (
<motion.li
whileFocus={{ scale: 1.05 }}
transition={{ duration: 0.2 }}
/>
));
}
通过:
function Menu() {
return items.map(item => (
<li className={styles.menuItem} /> // 仅使用 CSS :focus-visible
));
}
none-context-menu-entrance上下文菜单进入时不应有动画(仅退出时有)。
不通过:
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0 }}
/>
通过:
<motion.div exit={{ opacity: 0, scale: 0.95 }} />
审查文件时,以以下格式输出检查结果:
文件:行号 - [规则ID] 问题描述
示例:
components/drawer/index.tsx:45 - [spring-for-gestures] 拖拽交互使用了缓动函数而非弹簧动画
components/modal/styles.module.css:23 - [easing-entrance-ease-out] 模态框进入动画使用了 ease-in
在检查结果后,输出一个汇总:
| 规则 | 数量 | 严重程度 |
|---|---|---|
spring-for-gestures | 2 | 高 |
easing-entrance-ease-out | 1 | 中 |
duration-max-300ms | 3 | 中 |
| 交互 | 时序 | 类型 |
|---|---|---|
| 拖拽释放 | 弹簧动画 | stiffness: 500, damping: 30 |
| 按钮按压 | 150ms | ease |
| 模态框进入 | 200ms | ease-out |
| 模态框退出 | 150ms | ease-in |
| 页面切换 | 250ms | ease-in-out |
| 进度条 | 可变 | linear |
| 打字反馈 | 0ms | 无动画 |
每周安装量
152
代码仓库
GitHub 星标数
641
首次出现
2026年1月26日
安全审计
安装于
opencode130
cursor126
codex123
claude-code117
gemini-cli115
github-copilot105
Review animation code for correct timing function selection based on interaction type.
file:line format| Priority | Category | Prefix |
|---|---|---|
| 1 | Spring Selection | spring- |
| 2 | Easing Selection | easing- |
| 3 | Duration | duration- |
| 4 | No Animation | none- |
Ask: Is this motion reacting to the user, or is the system speaking?
| Motion Type | Best Choice | Why |
|---|---|---|
| User-driven (drag, flick, gesture) | Spring | Survives interruption, preserves velocity |
| System-driven (state change, feedback) | Easing | Clear start/end, predictable timing |
| Time representation (progress, loading) | Linear | 1:1 relationship between time and progress |
| High-frequency (typing, fast toggles) | None | Animation adds noise, feels slower |
spring-for-gesturesGesture-driven motion (drag, flick, swipe) must use springs.
Fail:
<motion.div
drag="x"
transition={{ duration: 0.3, ease: "easeOut" }}
/>
Pass:
<motion.div
drag="x"
transition={{ type: "spring", stiffness: 500, damping: 30 }}
/>
spring-for-interruptibleMotion that can be interrupted must use springs.
Fail:
// User can click again mid-animation
<motion.div
animate={{ x: isOpen ? 200 : 0 }}
transition={{ duration: 0.3 }}
/>
Pass:
<motion.div
animate={{ x: isOpen ? 200 : 0 }}
transition={{ type: "spring", stiffness: 400, damping: 25 }}
/>
spring-preserves-velocityWhen velocity matters, use springs to preserve input energy.
Fail:
// Fast flick and slow flick animate identically
onDragEnd={(e, info) => {
animate(target, { x: 0 }, { duration: 0.3 });
}}
Pass:
// Fast flick moves faster than slow flick
onDragEnd={(e, info) => {
animate(target, { x: 0 }, {
type: "spring",
velocity: info.velocity.x,
});
}}
spring-params-balancedSpring parameters must be balanced; avoid excessive oscillation.
Fail:
transition={{
type: "spring",
stiffness: 1000,
damping: 5, // Too low - excessive bounce
}}
Pass:
transition={{
type: "spring",
stiffness: 500,
damping: 30, // Balanced - settles quickly
}}
easing-for-state-changeSystem-initiated state changes should use easing curves.
Fail:
// Toast notification using spring
<motion.div
animate={{ y: 0 }}
transition={{ type: "spring" }}
/>
// Feels restless for a simple announcement
Pass:
<motion.div
animate={{ y: 0 }}
transition={{ duration: 0.2, ease: "easeOut" }}
/>
easing-entrance-ease-outEntrances must use ease-out (arrive fast, settle gently).
Fail:
.modal-enter {
animation-timing-function: ease-in;
}
Pass:
.modal-enter {
animation-timing-function: ease-out;
}
easing-exit-ease-inExits must use ease-in (build momentum before departure).
Fail:
.modal-exit {
animation-timing-function: ease-out;
}
Pass:
.modal-exit {
animation-timing-function: ease-in;
}
easing-transition-ease-in-outView/mode transitions use ease-in-out for neutral attention.
Pass:
.page-transition {
animation-timing-function: ease-in-out;
}
easing-linear-only-progressLinear easing only for progress bars and time representation.
Fail:
.card-slide {
transition: transform 200ms linear; /* Mechanical feel */
}
Pass:
.progress-bar {
transition: width 100ms linear; /* Honest time representation */
}
duration-press-hoverPress and hover interactions: 120-180ms.
Fail:
.button:hover {
transition: background-color 400ms;
}
Pass:
.button:hover {
transition: background-color 150ms;
}
duration-small-stateSmall state changes: 180-260ms.
Pass:
.toggle {
transition: transform 200ms ease;
}
duration-max-300msUser-initiated animations must not exceed 300ms.
Fail:
<motion.div transition={{ duration: 0.5 }} />
Pass:
<motion.div transition={{ duration: 0.25 }} />
duration-shorten-before-curveIf animation feels slow, shorten duration before adjusting curve.
Fail (common mistake):
/* Trying to fix slowness with sharper curve */
.element {
transition: 400ms cubic-bezier(0, 0.9, 0.1, 1);
}
Pass:
/* Fix slowness with shorter duration */
.element {
transition: 200ms ease-out;
}
none-high-frequencyHigh-frequency interactions should have no animation.
Fail:
// Animated on every keystroke
function SearchInput() {
return (
<motion.div animate={{ scale: [1, 1.02, 1] }}>
<input onChange={handleSearch} />
</motion.div>
);
}
Pass:
function SearchInput() {
return <input onChange={handleSearch} />;
}
none-keyboard-navigationKeyboard navigation should be instant, no animation.
Fail:
function Menu() {
return items.map(item => (
<motion.li
whileFocus={{ scale: 1.05 }}
transition={{ duration: 0.2 }}
/>
));
}
Pass:
function Menu() {
return items.map(item => (
<li className={styles.menuItem} /> // CSS :focus-visible only
));
}
none-context-menu-entranceContext menus should not animate on entrance (exit only).
Fail:
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0 }}
/>
Pass:
<motion.div exit={{ opacity: 0, scale: 0.95 }} />
When reviewing files, output findings as:
file:line - [rule-id] description of issue
Example:
components/drawer/index.tsx:45 - [spring-for-gestures] Drag interaction using easing instead of spring
components/modal/styles.module.css:23 - [easing-entrance-ease-out] Modal entrance using ease-in
After findings, output a summary:
| Rule | Count | Severity |
|---|---|---|
spring-for-gestures | 2 | HIGH |
easing-entrance-ease-out | 1 | MEDIUM |
duration-max-300ms | 3 | MEDIUM |
| Interaction | Timing | Type |
|---|---|---|
| Drag release | Spring | stiffness: 500, damping: 30 |
| Button press | 150ms | ease |
| Modal enter | 200ms | ease-out |
| Modal exit | 150ms | ease-in |
| Page transition | 250ms | ease-in-out |
Weekly Installs
152
Repository
GitHub Stars
641
First Seen
Jan 26, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
opencode130
cursor126
codex123
claude-code117
gemini-cli115
github-copilot105
agentation - AI智能体可视化UI反馈工具,连接人眼与代码的桥梁
5,400 周安装
| Progress bar | varies | linear |
| Typing feedback | 0ms | none |