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.

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

Source material given with the request

.account-menu { position: absolute; top: calc(100% + 8px); right: 0; transform: scale(0); transform-origin: center; transition: all 400ms ease-in; } .account-menu.is-open { transform: scale(1); }
with the skill
## Review: `.account-menu` | Before | After | Why | | --- | --- | --- | | `transform: scale(0)` | `transform: scale(0.95); opacity: 0` | Nothing in the real world appears from nothing. Starting from a visible shape with a fade feels natural. | | `transform-origin: center` | `transform-origin: top right` | The menu is anchored under a top-right button (`top: 100%; right: 0`), so it should grow out of the trigger. This is a popover, not a modal. | | `transition: all 400ms ease-in` | `transition: transform 180ms var(--ease-out), opacity 180ms var(--ease-out)` | Name exact properties instead of `all`. Only `transform` and `opacity` stay on the GPU path. | | `ease-in` | `cubic-bezier(0.23, 1, 0.32, 1)` | `ease-in` starts slow, which is the moment the user is watching most closely, so it feels sluggish. A strong ease-out gives instant feedback. | | `400ms` duration | `180ms` open, `120ms` close | UI animations should stay under 300ms. Dropdowns belong in the 150-250ms range. | | Same timing for enter and exit | Faster exit than enter | Slow where the user is deciding, fast where the system is responding. Closing should never make the user wait. | | Closed state is only `scale(0)` | Add `visibility: hidden; pointer-events: none` when closed | Once `opacity: 0` is used, the invisible menu would still be clickable and focusable. Visibility must flip after the fade-out finishes. | | No reduced-motion handling | `@media (prefers-reduced-motion: reduce)` drops the scale and keeps the fade | Reduced motion means gentler motion, not none. Opacity still aids comprehension. | ### Revised CSS ```css :root { --ease-out: cubic-bezier(0.23, 1, 0.32, 1); }
Show the rest (46 more lines)
.account-menu { position: absolute; top: calc(100% + 8px); right: 0; opacity: 0; transform: scale(0.95); transform-origin: top right; visibility: hidden; pointer-events: none; /* Exit: fast. Visibility flips after the fade completes. */ transition: transform 120ms var(--ease-out), opacity 120ms var(--ease-out), visibility 0s linear 120ms; } .account-menu.is-open { opacity: 1; transform: scale(1); visibility: visible; pointer-events: auto; /* Enter: slightly slower, visibility flips immediately. */ transition: transform 180ms var(--ease-out), opacity 180ms var(--ease-out), visibility 0s; } @media (prefers-reduced-motion: reduce) { .account-menu, .account-menu.is-open { transform: none; transition-property: opacity, visibility; } } ``` ### Notes - **Transitions, not keyframes:** you're already using a transition, which is right. If a user double-clicks the trigger, it retargets from its current state instead of restarting. - **Keyboard opens:** if people commonly open this with a shortcut, consider skipping the animation for keyboard-initiated opens (for example, `transition-duration: 0ms` via a `data-keyboard` attribute). For a plain account menu, 180ms is fine. - **Trigger feedback:** give the header button `transition: transform 160ms ease-out` and `:active { transform: scale(0.97) }` so the press registers before the menu opens. - **Check it in slow motion:** temporarily set durations to 2-5x and confirm the menu visibly grows from the button's corner. If the origin looks off, check where the menu's box sits relative to the trigger.
without
# Review of `.account-menu` ## Main issues **1. `transform-origin: center` is wrong for this placement.** The menu is anchored to a button at the top-right, so it should grow out of that corner. With `center`, it appears to balloon from the middle of nowhere. Use `top right`. **2. `scale(0)` hides the menu visually but not functionally.** The links inside are still in the tab order and may still be exposed to screen readers. Keyboard users can tab into an invisible menu. Pair the transform with `visibility: hidden` (or use `hidden`/`inert`/`display: none`). **3. The timing is too slow, and the easing is backwards.** - 400ms feels sluggish for a dropdown. 120-200ms is the usual range. - `ease-in` starts slow, which feels laggy for something responding to a click. Entering elements should use `ease-out`. - Closing should usually be a bit faster than opening. **4. `transition: all` is a smell.** It will animate anything that changes later (colors, width, etc.) and can cause unexpected effects and unnecessary work. List the properties explicitly. **5. Scaling from 0 is visually harsh.** Popovers feel more natural starting around `scale(0.95)` combined with a fade.
Show the rest (53 more lines)
## Smaller things - Make sure the parent wrapper has `position: relative`, or `top`/`right` will resolve against some other ancestor. - There's no `z-index`, so the menu may render under other header or page content. - Consider `min-width` and `max-width: calc(100vw - 16px)` so it doesn't overflow on small screens. - Add a `prefers-reduced-motion` fallback. ## Suggested revision ```css .account-menu { position: absolute; top: calc(100% + 8px); right: 0; z-index: 100; opacity: 0; visibility: hidden; transform: scale(0.95); transform-origin: top right; /* closing: quick, and delay visibility until the fade finishes */ transition: opacity 120ms ease-out, transform 120ms ease-out, visibility 0s linear 120ms; } .account-menu.is-open { opacity: 1; visibility: visible; transform: scale(1); /* opening: slightly longer, visibility flips immediately */ transition: opacity 160ms ease-out, transform 160ms ease-out, visibility 0s; } @media (prefers-reduced-motion: reduce) { .account-menu, .account-menu.is-open { transition: none; } } ``` ## Non-CSS things to check - The button should have `aria-expanded` (toggled with `.is-open`) and `aria-controls` pointing at the menu's id. - Close on `Escape` and on outside click, and return focus to the button when closing via keyboard. - The wrapper around the button and menu needs `position: relative`.

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.