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를 다듬는 디자인 엔지니어의 철학을 규칙으로 옮겼습니다. 애니메이션에 대한 질문을 순서대로 던집니다. 애초에 움직여야 하는가(하루에 수백 번 쓰는 키보드 동작에는 절대 안 됨), 목적은 무엇인가, 어떤 이징인가, 얼마나 빠른가. CSS 기본 이징 대신 강한 맞춤 곡선을 쓰고, UI는 약 300ms 이내로 유지하며, 스프링을 써야 할 때를 설명합니다. 컴포넌트 세부(눌림 피드백, 0에서 시작하는 확대 금지, 트리거에서 나오는 팝오버, 툴팁 지연 규칙, 키프레임 대신 트랜지션), clip-path 기법, 드래그와 제스처, 성능 규칙(transform과 opacity만 애니메이션), 모션 줄이기, 터치 기기의 호버 처리도 포함합니다. 검토는 반드시 'Before / After / Why' 표와 체크리스트로 돌려줍니다.
결과물
검토 표, 우선순위 목록, 각 값의 이유가 붙은 CSS 또는 JSX 제안.
이런 때 좋습니다
인터페이스의 손맛을 살리고 싶은 프런트엔드 개발자와 디자이너, 모션과 컴포넌트 다듬기에 일관된 검토 기준을 두려는 팀.
낮은 위험:지침만 있는 파일로 스크립트가 없고, 네트워크에 접속하지 않으며, 파일을 쓰지 않습니다. 주관이 뚜렷한 프런트엔드 조언이며 수치는 작성자 본인의 표에서 나온 것이므로 절대 법칙이 아니라 강한 기본값으로 보세요. 붙여 넣은 코드로 한 번 시험했고, 정해진 표로 답하며 명령 팔레트의 애니메이션 문제를 짚고 우선순위를 줬습니다. 실행이나 렌더링은 하지 않았습니다. 질문 없이 호출하면 짧은 인사만 합니다. 작성자 Emil Kowalski, MIT.