This skill should be used when building UI components, adding animations, or reviewing design decisions with Emil Kowalski's design philosophy. Triggers include "make it feel Linear", "emil style", "tasteful animation", or when designing interactive components that need craft and restraint.
Inherits all available tools
Additional assets for this skill
This skill inherits all available tools. When active, it can use any tool Claude has access to.
references/animation-principles.mdreferences/component-patterns.mdreferences/linear-voice.mddesign philosophy from Emil Kowalski (Linear, ex-Vercel) and the Linear design system. restraint, craft, intentionality.
"you are animating more often than you should"
| principle | meaning |
|---|---|
| restraint | every animation must earn its place |
| elimination | excellence through removal, not addition |
| intentionality | every pixel has purpose |
| craft | performance is polish |
| focus | one thing, done perfectly |
| use | skip |
|---|---|
| building interactive components | static content |
| adding motion/transitions | utility functions |
| reviewing UI design decisions | backend work |
| "make it feel Linear" | rough prototypes |
| polishing existing UI | first pass implementation |
is it a state change the user initiated?
├── yes → does animation provide feedback?
│ ├── yes → animate (keep under 300ms)
│ └── no → skip animation
└── no → is it drawing attention to something important?
├── yes → animate subtly (opacity, not bounce)
└── no → skip animation
what's the interaction?
├── button press → scale(0.98) + darker bg, 100ms
├── hover state → background shift only, 150ms
├── toast entrance → slide + fade, 200ms ease-out
├── toast exit → fade only, 200ms ease-in
├── modal open → opacity + scale(0.95→1), 200ms ease-out
├── modal close → opacity only, 150ms ease-in
├── drawer open → spring physics, gesture-driven
├── drawer close → spring + velocity-aware snap
├── tab switch → translateX indicator, 300ms ease-out
└── loading → opacity pulse or subtle motion
interaction type?
├── micro (hover, press) → 100-150ms
├── standard (fade, slide) → 200ms
├── navigation (tabs, pages) → 300ms
├── complex (drawer, modal) → 200-400ms with spring
└── exit → 20-30% faster than entrance
| property | value | source |
|---|---|---|
| exit animation | 200ms | TIME_BEFORE_UNMOUNT |
| swipe threshold | 45px | SWIPE_THRESHOLD |
| velocity threshold | 0.11 px/ms | dismissal trigger |
| dampening | 1 / (1.5 + abs(delta)/20) | drag resistance |
| default width | 356px | --width |
| gap between toasts | 14px | --gap |
| toast lifetime | 4000ms | TOAST_LIFETIME |
| property | value | source |
|---|---|---|
| transition duration | 500ms base | TRANSITIONS.DURATION |
| close threshold | 0.25 (25%) | CLOSE_THRESHOLD |
| fast swipe velocity | 0.05 | minimum for "fast" |
| swipe start (touch) | 10px | threshold |
| swipe start (pointer) | 2px | threshold |
| scroll lock timeout | 500ms | SCROLL_LOCK_TIMEOUT |
| border radius animation | 8px → 0px | during drag |
| translate offset | 14px | scale adjustment |
from ~/Developer/components/CLAUDE.md:
| pattern | value | usage |
|---|---|---|
| standard timing | 200ms | most transitions |
| easing | ease-out | entrances |
| active state | 1px translateY | not scale |
| first tooltip delay | 400ms | then instant |
| click cooldown | 500ms | prevent double-action |
| hover transition | 150ms | subtle bg change |
// 200ms transitions, subtle bg changes
// persistent hover states
// instant submenu transitions (no delay)
// 300ms animated underline
// translateX for indicator movement
// auto-positioning on route change
// 400ms delay for first tooltip
// instant switch between tooltips (skip animation)
// 500ms click suppression cooldown
// auto-close on scroll
// GPU-accelerated translate3d
// staggered entrance animations
// deterministic randomization (seeded)
// bouncy easing for hover spread
// entrance: slide from edge + fade
transform: translateY(100%);
opacity: 0;
// → animate to
transform: translateY(0);
opacity: 1;
transition: all 200ms ease-out;
// exit: fade only (no slide)
opacity: 0;
transition: opacity 200ms ease-in;
// stacking: newest on top
// dismiss: swipe OR timeout (4s default)
// no bounce, no overshoot
// use spring physics, not cubic-bezier
const spring = {
damping: 25,
stiffness: 350,
};
// snap points: discrete heights
const SNAP_POINTS = [0.25, 0.5, 0.9]; // of viewport
// dismiss: drag below 25% OR fast swipe down
// velocity-aware: fast swipe skips intermediate snaps
// entrance: overlay fade + content scale
overlay: { opacity: [0, 1], duration: 200 }
content: {
opacity: [0, 1],
scale: [0.95, 1],
duration: 200,
easing: 'ease-out'
}
// exit: faster than entrance
exit: { opacity: 0, duration: 150, easing: 'ease-in' }
// focus trap inside
// dismiss: escape, click outside, explicit close
// no slide (that's a drawer)
// press: scale + darker bg
:active {
transform: scale(0.98);
background: var(--bg-pressed);
transition: all 100ms;
}
// loading: spinner replaces content, SAME dimensions
// disabled: 50% opacity, no transitions
// never animate width/padding
GPU-accelerated (always prefer):
opacitytransform (translate, scale, rotate)avoid animating:
width, height → causes layout thrashtop, left, right, bottom → causes layoutmargin, padding → causes layoutborder-width → causes paintbox-shadow → expensive compositefilter → expensive| anti-pattern | why it's wrong | fix |
|---|---|---|
| bounce on entrance | gratuitous, not earned | ease-out only |
| spring on buttons | buttons aren't physical objects | scale(0.98) |
| stagger > 10 items | feels like loading spinner | instant after 10 |
| parallax | almost always gratuitous | remove |
| scroll animations | janky, kills perf | avoid |
| exit slide | users don't watch exits | fade only |
| > 400ms duration | feels sluggish | shorten |
| competing motions | chaos | one thing at a time |