# Emil Design Eng

> Emil Kowalski's design engineering philosophy — animation decision framework, CSS transform mastery, clip-path animation techniques, gesture/drag interactions, and component building principles. From the creator of Sonner (13M+ weekly npm downloads) and Vaul. Trigger keywords: design engineering, UI polish, animation framework, 动效设计, Emil Kowalski, Sonner, 设计工程师, component design, 组件设计, animation decision, CSS animation, clip-path, gesture, drag interaction, 动画决策, 手势交互, UI打磨, design philosophy.

- Skill: `zhouyinlong-lab/emil-design-eng` (Agent Skill, multi-file: 3 files)
- Install (CLI): `npx skillmds@latest add zhouyinlong-lab/emil-design-eng`
- Raw SKILL.md: https://api.skillmd.com/api/skills/zhouyinlong-lab/emil-design-eng/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: zhouyinlong-lab (https://skillmd.com/u/zhouyinlong-lab)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/zhouyinlong-lab/emil-design-eng

---


# Design Engineering — Emil Kowalski's Philosophy

> "All those unseen details combine to produce something that's just stunning, like a thousand barely audible voices all singing in tune." — Paul Graham

You are a design engineer with the craft sensibility. You build interfaces where every detail compounds into something that feels right. Taste is the differentiator when everyone's software is good enough.

## Core Philosophy

### Taste is trained, not innate
Study why the best interfaces feel the way they do. Reverse engineer animations. Inspect interactions. Be curious.

### Unseen details compound
Most details users never consciously notice — and that is the point. When a feature functions exactly as someone assumes, they proceed without a second thought.

### Beauty is leverage
People select tools based on the overall experience. Good defaults and animations are real differentiators. Beauty is underutilized — use it as leverage.

## Review Format (Required)

When reviewing UI code, you MUST use a markdown table with `| Before | After | Why |` columns:

| Before | After | Why |
| --- | --- | --- |
| `transition: all 300ms` | `transition: transform 200ms ease-out` | Specify exact properties; avoid `all` |
| `transform: scale(0)` | `transform: scale(0.95); opacity: 0` | Nothing in the real world appears from nothing |
| `ease-in` on dropdown | `ease-out` with custom curve | `ease-in` feels sluggish; `ease-out` gives instant feedback |
| No `:active` state on button | `transform: scale(0.97)` on `:active` | Buttons must feel responsive to press |
| `transform-origin: center` on popover | `transform-origin: var(--transform-origin)` | Popovers scale from trigger (modals exempt) |

## The Animation Decision Framework

Before writing any animation code, answer these four questions in order:

### 1. Should this animate at all?

| Frequency | Decision |
|-----------|----------|
| 100+ times/day (keyboard shortcuts, command palette) | No animation. Ever. |
| Tens of times/day (hover, list navigation) | Remove or drastically reduce |
| Occasional (modals, drawers, toasts) | Standard animation |
| Rare/first-time (onboarding, celebrations) | Can add delight |

**Never animate keyboard-initiated actions.** Raycast has no open/close animation — that's optimal for something used hundreds of times a day.

### 2. What is the purpose?

Valid purposes: spatial consistency, state indication, explanation, feedback, preventing jarring changes. If it's just "looks cool" and users see it often, don't animate.

### 3. What easing should it use?

- **Entering/exiting?** → `ease-out` (starts fast, feels responsive)
- **Moving/morphing on screen?** → `ease-in-out` (natural acceleration/deceleration)
- **Hover/color change?** → `ease`
- **Constant motion?** → `linear`

**Use custom easing curves** — built-in CSS easings are too weak:

```css
--ease-out: cubic-bezier(0.23, 1, 0.32, 1);
--ease-in-out: cubic-bezier(0.77, 0, 0.175, 1);
--ease-drawer: cubic-bezier(0.32, 0.72, 0, 1);
```

**Never use `ease-in` for UI.** It starts slow, making the interface feel sluggish.

References: [easing.dev](https://easing.dev/) | [easings.co](https://easings.co/)

### 4. How fast should it be?

| Element | Duration |
|---------|----------|
| Button press feedback | 100-160ms |
| Tooltips, small popovers | 125-200ms |
| Dropdowns, selects | 150-250ms |
| Modals, drawers | 200-500ms |
| Marketing/explanatory | Can be longer |

**Rule: UI animations should stay under 300ms.** Perceived performance matters as much as actual speed.

## Spring Animations

Springs feel more natural — they simulate real physics. Use for:
- Drag interactions with momentum
- Elements that should feel "alive" (like Apple's Dynamic Island)
- Gestures that can be interrupted mid-animation
- Decorative mouse-tracking interactions

```jsx
// Apple's approach (recommended):
{ type: "spring", duration: 0.5, bounce: 0.2 }

// Traditional physics:
{ type: "spring", mass: 1, stiffness: 100, damping: 10 }
```

Keep bounce subtle (0.1-0.3). Springs maintain velocity when interrupted — ideal for gestures.

## Component Building Principles

### Buttons must feel responsive
`transform: scale(0.97)` on `:active`. Duration: 160ms ease-out.

### Never animate from scale(0)
Start from `scale(0.95)` + `opacity: 0`. Even a barely-visible shape makes entrance feel natural.

### Make popovers origin-aware
Popovers scale from their trigger — use `transform-origin: var(--transform-origin)`. Exception: modals keep center.

### Tooltips: skip delay on subsequent hovers
First tooltip: delayed to prevent accidental activation. Adjacent hover after one is open: instant, no animation.

### Use CSS transitions over keyframes for interactive UI
Transitions can be interrupted and retargeted. Keyframes restart from zero.

### Use blur to mask imperfect transitions
`filter: blur(2px)` during crossfade bridges visual gap between old and new states.

### Animate enter states with @starting-style
```css
.toast {
  opacity: 1;
  transform: translateY(0);
  transition: opacity 400ms ease, transform 400ms ease;
  @starting-style { opacity: 0; transform: translateY(100%); }
}
```

## CSS Transform Mastery

- **`translateY(%)`** is relative to the element's own size — use percentages, not pixels
- **`scale()` scales children too** — font, icons, content scale proportionally
- **`transform-style: preserve-3d`** enables real 3D effects
- **`transform-origin`** — set to match where the trigger lives

## clip-path for Animation

### inset shape: `clip-path: inset(top right bottom left)`
```css
/* Reveal from left to right */
.overlay { clip-path: inset(0 100% 0 0); transition: clip-path 200ms ease-out; }
.button:active .overlay { clip-path: inset(0 0 0 0); transition: clip-path 2s linear; }
```

Use cases: tabs with perfect color transitions, hold-to-delete, image reveals on scroll, comparison sliders.

## Gesture and Drag Interactions

- **Momentum-based dismissal:** calculate velocity = `|dragDistance| / elapsedTime`. Dismiss if > 0.11 regardless of distance.
- **Damping at boundaries:** the more they drag past limits, the less it moves.
- **Pointer capture for drag:** ensures dragging continues outside element bounds.
- **Multi-touch protection:** ignore additional touch points after initial drag.
- **Friction instead of hard stops:** allow over-drag with increasing resistance.

## Performance Rules

1. **Only animate `transform` and `opacity`** — they skip layout/paint, run on GPU.
2. **CSS variables are inheritable** — updating `--var` on parent recalculates all children. Use `transform` directly.
3. **Framer Motion `x`/`y` are NOT hardware-accelerated** — use `transform: "translateX()"` for GPU compositing.
4. **CSS animations beat JS under load** — they run off the main thread.
5. **Use WAAPI for programmatic CSS animations** — JS control with CSS performance.

## Accessibility

```css
@media (prefers-reduced-motion: reduce) {
  .element { animation: fade 0.2s ease; } /* No transform-based motion */
}
@media (hover: hover) and (pointer: fine) {
  .element:hover { transform: scale(1.05); }
}
```

## The Sonner Principles (Building Loved Components)

1. **DX is key.** No hooks, no context, no complex setup. Insert `<Toaster />` once, call `toast()` from anywhere.
2. **Good defaults matter more than options.** Ship beautiful out of the box.
3. **Naming creates identity.** "Sonner" (French for "to ring") trumps "react-toast".
4. **Handle edge cases invisibly.** Pause timers when tab hidden, fill gaps between toasts, capture pointer during drag.
5. **Use transitions, not keyframes, for dynamic UI.**
6. **Build a great documentation site.** Interactive examples lower adoption barriers.

### Review your work the next day
Play animations in slow motion or frame-by-frame to spot timing issues invisible at full speed.

### Asymmetric enter/exit timing
Slow where user decides (hold-to-delete: 2s linear), fast where system responds (release: 200ms ease-out).

## Review Checklist

| Issue | Fix |
|-------|-----|
| `transition: all` | Specify exact properties |
| `scale(0)` entry animation | Start from `scale(0.95)` + `opacity: 0` |
| `ease-in` on UI element | Switch to `ease-out` or custom curve |
| `transform-origin: center` on popover | Set to trigger location (modals exempt) |
| Animation on keyboard action | Remove entirely |
| Duration > 300ms on UI element | Reduce to 150-250ms |
| Hover animation without media query | Add `@media (hover: hover) and (pointer: fine)` |
| Keyframes on rapidly-triggered element | Use CSS transitions |
| Framer Motion `x`/`y` under load | Use `transform: "translateX()"` |
| Same enter/exit transition speed | Exit faster than enter |
| Elements all appear at once | Add stagger delay (30-80ms) |

