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.
できること
UI の仕上げに関するデザインエンジニアの考え方をルールにしたものです。アニメーションの問いを順に立てます。そもそも動かすべきか(1日に何百回も使うキーボード操作では動かさない)、目的は何か、どのイージングか、どれだけ速くするか。CSS 標準のイージングではなく強めの独自カーブを使い、UI は約300ms以内に収め、スプリングを使う場面も説明します。コンポーネントの細部(押下フィードバック、0からの拡大をしない、トリガーを起点に出るポップオーバー、ツールチップの遅延ルール、キーフレームよりトランジション)、clip-path の技、ドラッグとジェスチャー、パフォーマンス(transform と opacity だけをアニメーションさせる)、モーション低減、タッチでのホバー制御も含みます。レビューは必ず「Before / After / Why」の表とチェックリストで返します。
出力
レビュー表、優先度リスト、各数値の理由を添えた CSS または JSX の提案。
こんな場面に
UI の手触りを良くしたいフロントエンド開発者やデザイナー、モーションとコンポーネントの仕上げに共通の基準を持ちたいチーム。
低リスク:指示のみのファイルで、スクリプトはなく、ネットワークにも接続せず、ファイルも書き込みません。主張の強いフロントエンドの助言で、数値は作者自身の表に基づくため、絶対の法則ではなく強い既定値として扱ってください。貼り付けたコードで一度試用し、決められた表で返し、コマンドパレットのアニメーションを指摘して優先度を付けました。何も実行・描画していません。質問なしで呼ぶと、短い挨拶だけを返します。作者 Emil Kowalski、MIT。