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 授權。