Here's the review.
| Before | After | Why |
|---|---|---|
.btn { transition: all 300ms; } |
transition: transform 160ms ease-out; plus .btn:active { transform: scale(0.97); } |
all animates properties you didn't intend to animate and can trigger layout and paint. 300ms is too slow for button feedback (aim for 100-160ms). The button also has no press state. |
.menu { transform: scale(0) } |
transform: scale(0.95); opacity: 0; and .menu.open { transform: scale(1); opacity: 1; } |
Nothing in the real world appears from nothing. scale(0) makes the menu pop out of a point. |
transform-origin: center on the profile dropdown |
transform-origin: top right (or the trigger's side), or var(--transform-origin) if you use Base UI or Radix |
A popover should grow from its trigger. Only modals stay centered. |
transition: transform 500ms ease-in |
transition: transform 180ms cubic-bezier(0.23, 1, 0.32, 1), opacity 180ms ease-out; |
ease-in starts slow, which is exactly when the user is watching. 500ms is far too long for a dropdown (150-250ms). Make the close faster than the open, around 120ms. |
.card:hover { transform: scale(1.05); } |
Wrap it in @media (hover: hover) and (pointer: fine), drop to about scale(1.02), and add transition: transform 200ms ease-out on .card |
Touch devices fire hover on tap, so the card sticks scaled. There is also no transition, so the hover snaps. 1.05 is heavy for a card, and it resizes the content and can cause neighbors to look cramped. |
.row { animation: slideIn 600ms ease-in-out forwards; } on 40 items at once |
animation: slideIn 250ms cubic-bezier(0.23, 1, 0.32, 1) both; with animation-delay: calc(var(--i) * 30ms), and cap the stagger (for example min(var(--i), 8)) |
Entering elements need ease-out, not ease-in-out. 600ms is too long. All 40 rows moving in unison reads as one block, so stagger them by 30-80ms. Don't let the stagger run for seconds (40 × 30ms is 1.2s), so cap it. Never block interaction while it plays. |
.row { opacity: 0; ... } as the base style |
Put the hidden state in the keyframe's from and use animation-fill-mode: both |
If the animation fails or is disabled, rows stay invisible forever. Also consider @starting-style or transitions if rows get added dynamically, since keyframes restart from zero when interrupted. |
<motion.div animate={{ x: open ? 0 : -300 }} /> |
animate={{ transform: open ? "translateX(0)" : "translateX(-100%)" }} with an explicit transition, such as a spring (duration: 0.3, bounce: 0) |
The x shorthand runs on the main thread and drops frames under load. A hardcoded -300 breaks if the panel width changes, so percentages are safer. Springs also keep velocity if the user toggles mid-motion. |
| Cmd+K palette with a 400ms fade and slide | No animation. Render it instantly. | A keyboard shortcut used hundreds of times a day should feel instant. Raycast does this. If you want some softness, use at most a 100ms opacity-only fade, and none on close. |
| No reduced-motion handling anywhere | @media (prefers-reduced-motion: reduce) keeps opacity fades and removes the slide, scale and translate motion. In Motion, use useReducedMotion(). |
Reduced motion means gentler, not zero. Keep the fades that aid comprehension and remove the movement. |
Priorities
- Remove the Cmd+K animation. It has the biggest impact on how the app feels.
- Fix the dropdown (
scale(0),ease-in, 500ms, center origin), since it hits every issue at once. - Stagger and shorten the list animation.
- Clean up
transition: all, the hover gating, and the Motionxprop.
Suggested CSS for the dropdown and rows
:root { --ease-out: cubic-bezier(0.23, 1, 0.32, 1); }
.menu {
opacity: 0;
transform: scale(0.95);
transform-origin: top right; /* match the profile button's position */
transition: transform 120ms var(--ease-out), opacity 120ms ease-out;
}
.menu.open {
opacity: 1;
transform: scale(1);
transition-duration: 180ms;
}
.row {
animation: slideIn 250ms var(--ease-out) both;
animation-delay: calc(min(var(--i), 8) * 40ms);
}
@keyframes slideIn {
from { opacity: 0; transform: translateY(8px); }
}
The menu's closed state needs pointer-events: none or visibility: hidden so it isn't clickable while invisible.
Test run: a pasted CSS and JSX snippet with common animation mistakes. The model used the required Before, After, Why table, flagged the Cmd+K palette animation and gave a priority list. Nothing was run or rendered; the suggested values come from the Skill's own tables.
它做什么
把一位设计工程师对界面打磨的做法写成规则。它按顺序提动画问题:这个到底要不要动画(每天用上百次的键盘操作永远不要)、目的是什么、用哪种缓动、多快。它主张用自定义的强缓动曲线,而不是 CSS 内置的几种,让界面动画保持在约 300 毫秒以内,并说明何时该用弹簧。它还涵盖组件细节(按下反馈、不从 0 缩放、从触发点出现的弹出层、提示框的延迟规则、用过渡而不是关键帧)、clip-path 技巧、拖拽与手势、性能规则(只对 transform 和 opacity 做动画)、减少动态效果和触屏上的悬停处理。评审必须以“修改前 / 修改后 / 原因”的表格输出,并带一份检查清单。
输出
评审表格、优先级清单,以及带有每个数值理由的 CSS 或 JSX 建议。
适合什么场景
想让界面“手感对”的前端开发者和设计师,以及想为动效和组件打磨设立统一评审标准的团队。
低风险:纯指令文件,没有脚本,不联网,不写文件。它给出的是带明确主张的前端建议,数值来自作者自己的表格,请当作有力的默认值,而不是铁律。已用一段粘贴的代码试用一次:返回了规定的表格,指出了命令面板的动画问题并给出优先级。没有运行或渲染任何东西。没有带问题调用时,它只会回复一句简短的问候。作者 Emil Kowalski,MIT 协议。