with one click
design-engineering
// UI craftsmanship: animation rules, easing, micro-interactions, state polish. Triggers: animation, transition, ease-out, motion, micro-interaction, hover, loading state, UI polish.
// UI craftsmanship: animation rules, easing, micro-interactions, state polish. Triggers: animation, transition, ease-out, motion, micro-interaction, hover, loading state, UI polish.
[HINT] Download the complete skill directory including SKILL.md and all related files
| name | design-engineering |
| description | UI craftsmanship: animation rules, easing, micro-interactions, state polish. Triggers: animation, transition, ease-out, motion, micro-interaction, hover, loading state, UI polish. |
| effort | medium |
| user-invocable | false |
| allowed-tools | Read |
Based on Emil Kowalski's design engineering philosophy — UI polish, component craftsmanship, and the compound value of invisible details.
| Usage Pattern | Strategy |
|---|---|
| 100+ daily | No animation |
| Tens daily | Drastically reduce |
| Occasional | Standard animation |
| Rare/first-time | Add delight |
Never animate keyboard-initiated actions — they repeat hundreds of times daily, making animation feel sluggish.
Every animation requires justification: spatial consistency, state indication, explanation, user feedback, or preventing jarring transitions. "It looks cool" alone disqualifies frequent interactions.
| Direction | Easing | Why |
|---|---|---|
| Entering elements | ease-out | Immediate feedback |
| On-screen movement | ease-in-out | Natural acceleration |
| Hover/color changes | ease | Smooth transition |
| Constant motion | linear | No acceleration |
Critical: Abandon default CSS easings. Use custom curves:
/* Punchy entrance */
transition-timing-function: cubic-bezier(0.23, 1, 0.32, 1);
Never use ease-in — it delays initial movement exactly when attention peaks, making interfaces feel sluggish.
| Element | Timing |
|---|---|
| Button press | 100-160ms |
| Tooltips, small popovers | 125-200ms |
| Dropdowns, selects | 150-250ms |
| Modals, drawers | 200-500ms |
UI animations should stay under 300ms. Speed perception matters as much as actual speed.
button:active {
transform: scale(0.97);
}
Tactile feedback confirming interface responsiveness.
/* Bad */
.enter { transform: scale(0); opacity: 0; }
/* Good — natural entrance */
.enter { transform: scale(0.95); opacity: 0; }
Real-world objects don't vanish and reappear. Start from scale(0.95).
.popover {
transform-origin: var(--radix-popover-content-transform-origin);
}
Exception: modals keep centered origin (viewport-anchored, not trigger-anchored).
Initial tooltip includes delay; subsequent hovers skip both delay and animation via [data-instant] attribute — perceived speed without defeating accidental activation prevention.
/* Moves by own height — perfect for toasts, drawers */
transform: translateY(100%);
No hardcoded pixel values needed.
Unlike width/height, scale() proportionally scales content, icons, and text. Intentional feature, not a bug.
.orbit {
transform-style: preserve-3d;
}
Enables orbit animations and coin flips without JavaScript.
clip-path: inset(top right bottom left) creates rectangular clipping regions:
Stack tab lists, clip the active copy, animate clip-path on change for seamless color shifting.
.delete-overlay {
clip-path: inset(0 100% 0 0);
transition: clip-path 200ms ease-out; /* fast snap-back on release */
}
.delete-button:active .delete-overlay {
clip-path: inset(0 0 0 0);
transition: clip-path 2s linear; /* slow fill while holding */
}
.reveal {
clip-path: inset(0 0 100% 0); /* hidden */
}
.reveal.visible {
clip-path: inset(0 0 0 0); /* revealed */
}
Overlay images, clip top one by adjusting right inset based on drag position.
Only animate transform and opacity — these skip layout and paint. Animating padding, margin, height, width triggers full rendering pipeline.
Changing parent CSS variables recalculates styles on all children. Update transform directly on elements instead.
Shorthand properties (x, y, scale) use main-thread requestAnimationFrame, not GPU:
// Bad — main thread
<motion.div animate={{ x: 100 }} />
// Good — GPU accelerated
<motion.div animate={{ transform: "translateX(100px)" }} />
CSS animations run off-thread and remain smooth during page loads. Framer Motion drops frames when browser is busy. Use CSS for predetermined animations; JavaScript for dynamic, interruptible ones.
Keep opacity and color transitions (aid comprehension). Remove movement and position animations:
@media (prefers-reduced-motion: reduce) {
* {
animation-duration: 0.01ms !important;
transition-duration: 0.01ms !important;
}
}
@media (hover: hover) and (pointer: fine) {
.card:hover {
transform: translateY(-2px);
}
}
Touch triggers false hover positives — always gate hover animations.
Five principles (from Sonner, 13M+ weekly downloads):
Animation personality should match component identity. Playful components can bounce; professional dashboards stay crisp.
Deliberate actions stay slow (2s linear for hold-to-delete), system responses snap fast (200ms ease-out for release).
| Issue | Resolution |
|---|---|
transition: all | Specify properties: transition: transform 200ms ease-out |
scale(0) entries | Start scale(0.95) with opacity: 0 |
ease-in on UI | Switch to ease-out or custom curve |
Popover transform-origin: center | Use trigger-aware CSS variable |
| Animation on keyboard actions | Remove entirely |
| Duration > 300ms UI | Reduce to 150-250ms |
| Hover without media query | Add @media (hover: hover) and (pointer: fine) |
| Keyframes on rapid triggers | Use CSS transitions |
Framer x/y under load | Use transform: "translateX()" |
| Identical enter/exit speed | Make exit faster (e.g., 2s enter, 200ms exit) |
| Simultaneous element appearance | Stagger 30-80ms between items |
transition: all — animates unintended properties, hurts performanceease-in on UI elements — delays feedback when attention peaksheight/width/margin — triggers layout recalculation@media (hover: hover) — breaks touch devicestransition (transition: transform 200ms ease-out) — never transition: allease-out (or a custom curve) on UI appearances; ease-in delays feedback at the moment the user's attention peaksheight, width, margin, or top/left — animate transform and opacity only. Layout-triggering properties drop frames under load.transform: translateX(-50%) on an element that will animate opacity triggers a paint on every frame because the browser cannot composite the layer. Add will-change: transform, opacity to hint the compositor — but only during the animation, not permanently (it consumes GPU memory).x={100} prop is a shortcut for transform: translateX(100px), but under load it falls back to the main thread. Use the longhand style={{ transform: "translateX(100px)" }} for guaranteed compositor path.@media (prefers-reduced-motion: reduce) is widely supported but often forgotten. Users with vestibular disorders or pointer-device sensitivity will notice; include a reduced-motion override for every non-trivial animation.performance.mark and measure with explicit timestamps./a11y-validate/frontend-specialist agent/ux-designer agent/brand-voice