skills / design

emil-design-eng

what it does

This skill encodes Emil Kowalski's philosophy on UI polish, component design, animation decisions, and the invisible details that make software feel great.

Verified Before and after: 2 models 1 try per case View source Download .zip

Compare the answers

Answers from Claude Sonnet 5.5 and GPT-6.1 Sol · try 1 of 1

Same request, same model, once with the skill and once without. Read both and judge for yourself.

“Users say our Cmd+K command palette feels sluggish, even though it renders in under 10ms. Here's how it opens. What's going on?”

Source material given with the request

// CommandPalette.tsx (opened with Cmd+K, our power users open it dozens of times an hour) <AnimatePresence> {open && ( <motion.div initial={{ opacity: 0, scale: 0.8, y: -20 }} animate={{ opacity: 1, scale: 1, y: 0 }} exit={{ opacity: 0, scale: 0.8, y: -20 }} transition={{ type: "spring", stiffness: 120, damping: 8 }} className="palette" > <input autoFocus placeholder="Type a command" /> ... </motion.div> )} </AnimatePresence>
with the skill
## What's going on Your render time isn't the problem. The animation is. The palette is keyboard-triggered and opened dozens of times an hour, which puts it in the "no animation, ever" tier (Raycast has no open/close animation for this reason). Your spring makes it worse: - **Settle time is about 1s.** With `stiffness: 120, damping: 8`, the damping ratio is ~0.37, which is very underdamped. It overshoots by ~29% (scale goes past 1.0 to roughly 1.06) and wobbles for around a second. Users feel that wobble as lag even though the input is focusable immediately. - **The exit is animated too.** `AnimatePresence` keeps the palette mounted until the exit spring finishes. Closing and reopening quickly means fighting a half-dismissed palette. - **`scale: 0.8` and `y: -20` are a big visual jump.** It reads as a "pop", not a subtle entrance. - **Motion's `scale`/`y` shorthands run on the main thread.** If the app is busy, for example while loading command results, frames drop and it feels worse. ## Review | Before | After | Why | | --- | --- | --- | | Animated open on a Cmd+K action | No animation, render immediately | Keyboard-initiated, high-frequency actions should feel instant; animation makes them feel delayed and disconnected | | `type: "spring", stiffness: 120, damping: 8` | Remove the spring | Very underdamped: ~29% overshoot and ~1s to settle. Springs with bounce don't belong in utility UI | | `exit={{ opacity: 0, scale: 0.8, y: -20 }}` | No exit animation | `AnimatePresence` holds the node mounted until the exit finishes, so close feels slow and reopen can collide | | `scale: 0.8` start | If you animate at all, `scale: 0.97` with opacity | Large scale jumps feel like a pop. Use a barely perceptible start, and never go near `scale(0)` territory | | `y: -20` / `scale` shorthands | Full `transform` string, or CSS | Shorthands aren't hardware accelerated and drop frames under load | | Spring for a non-gesture, non-decorative element | Not needed | Springs suit drag,
Show the rest (29 more lines)
momentum, and interruptible gestures, not a command palette toggle | ## Fix ```tsx {open && ( <div className="palette"> <input autoFocus placeholder="Type a command" /> ... </div> )} ``` No `AnimatePresence`, no `motion.div`. It mounts and unmounts instantly, and the input is focused on the same frame as the keypress. If you want some polish for mouse-triggered opens (a menu button, say), split the behavior. Keep Cmd+K instant, and for pointer opens use a short transition with no bounce: ```css .palette { transition: opacity 120ms cubic-bezier(0.23, 1, 0.32, 1), transform 120ms cubic-bezier(0.23, 1, 0.32, 1); } .palette[data-starting-style] { opacity: 0; transform: scale(0.97); } ``` Even then, I'd leave the exit instant, or make it faster than the enter. For your power users, the best version is the one they never notice.
without
Render time isn't the problem. The animation is. The palette is mounted and interactive in about 10ms, but your spring takes about a second to settle, and users judge speed by when the UI looks done. ## The spring is very underdamped With `stiffness: 120, damping: 8` (mass 1): - Damping ratio ζ = 8 / (2·√120) ≈ **0.37**. Anything below 1 oscillates, and 0.37 is quite bouncy. - Overshoot is about **30%**, so the palette grows past full size, shrinks back, and wobbles. - The oscillation envelope decays like e^(−4t), so it takes about **1 second** to settle within 1%. It also starts from `scale: 0.8, y: -20`, a big visual distance to cover. The palette is mostly visible within 100-150ms, but it keeps wobbling for roughly 10x longer than a command palette should take. ## Exit makes it worse `AnimatePresence` keeps the element mounted until the exit animation finishes. Closing runs the same bouncy spring in reverse, so the palette lingers after Esc or after running a command. If someone reopens it during that window, they hit a half-exited element. For something opened dozens of times an hour, this adds up to a feeling of lag. ## Fix: make it nearly instant Command palettes (Raycast, Linear, Spotlight) use very short, non-bouncy transitions, or none at all.
Show the rest (25 more lines)
```tsx <AnimatePresence> {open && ( <motion.div initial={{ opacity: 0, scale: 0.98 }} animate={{ opacity: 1, scale: 1, transition: { duration: 0.12, ease: "easeOut" } }} exit={{ opacity: 0, transition: { duration: 0.06 } }} className="palette" > <input autoFocus placeholder="Type a command" /> ... </motion.div> )} </AnimatePresence> ``` Guidelines: - **Open:** 100-150ms, ease-out, with a subtle scale (0.97-0.99) and no `y` travel or bounce. - **Close:** 50-80ms fade, or no exit animation at all (drop the `exit` prop). Instant close feels best for frequent actions. - **If you want a spring,** make it critically damped and stiff, e.g. `{ type: "spring", stiffness: 600, damping: 40 }`. It settles in about 150ms with no overshoot. - **Respect `prefers-reduced-motion`** with `useReducedMotion()` and skip the transform. ## How to check Record the open in a screen capture or the Performance panel and count frames from keypress to visually stable. You'll likely see about 1s now and about 120ms after the change. Also make sure typing works immediately. `autoFocus` already handles that, but with the old animation the input looked like it wasn't ready, so users waited.

Answers are shown exactly as the model wrote them, from the first try, and nothing is picked by hand.

The models

2 models

Claude Sonnet 5.5

made by Anthropic

claude-sonnet-5-5 · revision not reported by the provider

1 try per case · tested 3 Nov 2026

GPT-6.1 Sol

made by OpenAI

gpt-6.1-sol · revision not reported by the provider

1 try per case · tested 3 Nov 2026

The tasks

written by Carte Blanche
dropdown-css-review

“Review the CSS for our account dropdown. It opens from a button in the top-right of the header.”

janky-bottom-sheet

“Our mobile bottom sheet stutters when it opens and while you drag it, especially on cheap Android phones. Why?”

rapid-toast-css

“Write the CSS for a toast that slides up from the bottom of the screen when it appears and fades out when dismissed. Several toasts can be added within a second, and they stack.”

sluggish-command-palette

“Users say our Cmd+K command palette feels sluggish, even though it renders in under 10ms. Here's how it opens. What's going on?”

sticky-hover-dead-press

“On phones our buttons sometimes stay enlarged after you tap them. They also feel kind of dead when you press them. Can you fix the CSS?”

Install

Download the .zip and unzip it. Put its files, in a folder named emil-design-eng, here:

Claude Code~/.claude/skills/Every project. For one project only, use .claude/skills/ inside it.
Codex~/.agents/skills/Every project. For one repository only, use .agents/skills/ inside it.
Cursor~/.cursor/skills/Cursor also reads the Claude Code and Codex folders above.

Then start a new session and ask for what the skill does. Other agents that read SKILL.md have their own skills folder; check their docs.

Coming after launch: get skills from inside your agent, no download needed.