# Motion Craft

> Design GPU-accelerated animations with Web Animations API (WAAPI), Scroll-Driven Animations (ScrollTimeline/ViewTimeline), FLIP technique, easing systems, and accessibility (prefers-reduced-motion). Use when user asks to create animations, transitions, scroll effects, page transitions, or interactive motion for web UIs. Do NOT use for canvas-based animations (Three.js, PixiJS), video editing, or…

- **Type:** Skill
- **Install:** `agentstack add skill-eliasoulkadi-shokunin-motion-craft`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [EliasOulkadi](https://agentstack.voostack.com/s/eliasoulkadi)
- **Installs:** 0
- **Category:** [Content & Media](https://agentstack.voostack.com/c/content-and-media)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [EliasOulkadi](https://github.com/EliasOulkadi)
- **Source:** https://github.com/EliasOulkadi/shokunin/tree/master/.pack/skills/motion-craft
- **Website:** https://eliasoulkadi.github.io/shokunin/

## Install

```sh
agentstack add skill-eliasoulkadi-shokunin-motion-craft
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# Motion Craft

Animations that communicate, not decorate. Every frame must earn its place.

Inspired by Emil Kowalski (Linear, Sonner, Vaul), Paul Lewis (Google Chrome), and the CSS Animation Working Group.

## Workflow

## The Animation Decision Framework

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

### 1. Should this animate at all?

| Frequency | Decision | Reason |
|-----------|----------|--------|
| 100+ times/day (keyboard shortcuts, command palette, search toggle) | No animation. Ever. | Animation makes frequent actions feel delayed. Raycast has no open/close animation. Optimal. |
| Tens of times/day (hover effects, list navigation) | Remove or drastically reduce | Users notice animation latency on repeated interactions |
| Occasional (modals, drawers, toasts) | Standard animation | The right place for motion |
| Rare / first-time (onboarding, confirmation, celebration) | Add delight | These moments earn longer, richer animations |

**Never animate keyboard-initiated actions.** These repeat hundreds of times daily.

### 2. What is the purpose?

Every animation must have a clear answer to "why does this animate?"

| Purpose | Example | Valid? |
|---------|---------|--------|
| Spatial consistency | Toast enters and exits from the same direction | ? |
| State indication | Button morphs to show "sent" | ? |
| Feedback | Button scales down on press, confirming the interface heard the user | ? |
| Preventing jarring changes | Elements fading in instead of appearing abruptly | ? |
| Explanation | Marketing animation showing how a feature works | ? |
| "It looks cool" AND the user sees it often | Gratuitous motion | ? |

### 3. What easing should it use?

| Scenario | Easing | CSS |
|----------|--------|-----|
| Element entering | ease-out (starts fast, feels responsive) | `cubic-bezier(0, 0, 0.2, 1)` |
| Element exiting | ease-in (starts slow, grabs attention briefly then accelerates away) | `cubic-bezier(0.4, 0, 1, 1)` |
| On-screen movement / morphing | ease-in-out | `cubic-bezier(0.4, 0, 0.2, 1)` |
| Hover / color change | ease | `cubic-bezier(0.25, 0.1, 0.25, 1)` |
| Constant motion (marquee, progress bar) | linear | `cubic-bezier(0, 0, 1, 1)` |
| UI interactions (buttons, tooltips, dropdowns) | strong ease-out | `cubic-bezier(0.23, 1, 0.32, 1)` |

**Critical: never use ease-in for UI entering animations.** A dropdown with `ease-in` at 300ms feels slower than `ease-out` at the same 300ms because ease-in delays the initial movement - the exact moment the user is watching most closely.

### 4. How fast should it be?

| Element | Duration | Easing |
|---------|----------|--------|
| Button press feedback (scale) | 100-160ms | `cubic-bezier(0.23, 1, 0.32, 1)` |
| Tooltip enter / exit | 125-200ms | `cubic-bezier(0.23, 1, 0.32, 1)` |
| Dropdown / select / popover | 150-250ms | `cubic-bezier(0.23, 1, 0.32, 1)` |
| Hover transform (scale, lift) | 200ms | `cubic-bezier(0.23, 1, 0.32, 1)` |
| Modal / dialog enter | 200-300ms | `cubic-bezier(0, 0, 0.2, 1)` |
| Modal / dialog exit | 150-200ms | `cubic-bezier(0.4, 0, 1, 1)` |
| Drawer / sheet enter | 200-400ms | `cubic-bezier(0.32, 0.72, 0, 1)` (iOS-like) |
| Toast enter | 300-400ms | `cubic-bezier(0.23, 1, 0.32, 1)` |
| Toast exit | 200-300ms | `cubic-bezier(0.4, 0, 1, 1)` |
| Page transition | 300-400ms | `cubic-bezier(0.4, 0, 0.2, 1)` |
| Scroll reveal (fade + slide up) | 400-800ms | `cubic-bezier(0.16, 1, 0.3, 1)` |
| Loading shimmer | 1000-1500ms (infinite) | linear |
| Hold-to-delete (press) | 2000ms | linear (deliberate) |
| Hold-to-delete (release) | 200ms | `cubic-bezier(0.23, 1, 0.32, 1)` (snappy) |
| Marketing / explanatory | 800-3000ms | variable, depends on narrative |

**Rule: UI animations should stay under 300ms.** A 180ms dropdown feels noticeably more responsive than a 400ms one.

### Custom easing curves

The built-in CSS `ease` / `ease-in` / `ease-out` are too weak. Use these stronger custom variants:

```css
:root {
  --ease-out-strong: cubic-bezier(0.23, 1, 0.32, 1);     /* UI interactions, Emil's signature */
  --ease-in-out-strong: cubic-bezier(0.77, 0, 0.175, 1);  /* On-screen movement */
  --ease-drawer: cubic-bezier(0.32, 0.72, 0, 1);          /* iOS-like drawer */
  --ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1);         /* Slow reveals, dramatic */
  --ease-out-quint: cubic-bezier(0.22, 1, 0.36, 1);       /* Alternative strong ease-out */
  --ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);       /* Slight overshoot, sparingly */
}
```

Sources: [easing.dev](https://easing.dev/), [easings.co](https://easings.co/)

---

## The 60fps Golden Rule

16.67ms per frame. Verify with Chrome DevTools Performance tab. Only animate `transform` and `opacity`.

| Property | GPU composite? | Triggers |
|----------|:--:|----------|
| `transform` (translate, scale, rotate) | ? Yes | Composite only |
| `opacity` | ? Yes | Composite only |
| `filter` | ?? Sometimes | Paint + Composite |
| `clip-path` | ?? Sometimes | Paint + Composite |
| `width`, `height`, `top`, `left`, `margin`, `padding`, `border-width` | ? No | Layout + Paint + Composite |
| `box-shadow` | ? No | Paint + Composite |
| `color`, `background-color` | ? No | Paint only |

## Review Format (Required)

When reviewing UI animation code, you MUST use a markdown table with Before | After | Why columns. Do NOT use a list with "Before:" and "After:" on separate lines.

### Correct format:

| Before | After | Why |
|--------|-------|-----|
| `transition: all 300ms` | `transition: transform 200ms ease-out` | Specify exact properties; avoid `all` which triggers layout recalculation |
| `transform: scale(0)` | `transform: scale(0.95); opacity: 0` | Nothing in the real world appears from nothing |
| `ease-in` on dropdown | `cubic-bezier(0.23, 1, 0.32, 1)` | `ease-in` starts slow - feels sluggish. Strong 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(--radix-popover-content-transform-origin)` | Popovers should scale from their trigger. Modals stay centered. |
| `transition: 300ms` without `prefers-reduced-motion` | `@media (prefers-reduced-motion: reduce) { transition-duration: 0.01ms }` | WCAG 2.1 requires reduced-motion support |
| `animate={{ x: 100 }}` in Framer Motion | `animate={{ transform: "translateX(100px)" }}` | Framer Motion `x`/`y` are NOT hardware-accelerated. Use `transform` string for GPU. |

**Wrong format (never do this):**
```
Before: transition: all 300ms
After: transition: transform 200ms ease-out
```

---

## CSS Transitions (preferred for UI)

CSS transitions are interruptible - they can be retargeted mid-animation. Keyframes restart from zero. For any interaction that can be triggered rapidly (toasts, toggles, dropdowns), transitions produce smoother results.

```css
.button {
  transition: transform 160ms ease-out;
}
.button:active {
  transform: scale(0.97);
}

.tooltip {
  transition: transform 125ms ease-out, opacity 125ms ease-out;
  transform-origin: var(--transform-origin);
}
```

### Asymmetric enter / exit timing

Enter can be slower (user is deciding), exit must be fast (system responding):

```css
.overlay {
  transition: clip-path 200ms ease-out;  /* release: fast */
}
.button:active .overlay {
  transition: clip-path 2s linear;       /* press: slow and deliberate */
}
```

### Tooltips: skip delay on subsequent hovers

```css
.tooltip {
  transition: transform 125ms ease-out, opacity 125ms ease-out;
  transform-origin: var(--transform-origin);
}
.tooltip[data-instant] {
  transition-duration: 0ms;
}
```

### Enter states with @starting-style (Chrome-only as of 2026, not yet Baseline)

```css
.toast {
  opacity: 1;
  transform: translateY(0);
  transition: opacity 400ms ease-out, transform 400ms ease-out;

  @starting-style {
    opacity: 0;
    transform: translateY(100%);
  }
}
```

> Note: `@starting-style` and scroll-driven animations (`animation-timeline: view()`, `animation-timeline: scroll()`) are Chrome-only as of 2026. Use JavaScript fallbacks for cross-browser support.

### Hold-to-delete pattern

```css
.overlay {
  clip-path: inset(0 100% 0 0);
  transition: clip-path 200ms ease-out;   /* release: fast */
}
.button:active .overlay {
  clip-path: inset(0 0 0 0);
  transition: clip-path 2s linear;        /* press: slow and deliberate */
}
```

---

## BUTTONS - Tactile Feedback

Every pressable element needs physical response.

```css
.button {
  transition: transform 160ms cubic-bezier(0.23, 1, 0.32, 1);
}
.button:active {
  transform: scale(0.97);
}
```

The scale must be subtle (0.95-0.98). Applied to all pressable elements - buttons, cards, list items, tabs.

---

## POPOVERS - Origin-Aware

Popovers scale from their trigger, not center. Modals stay centered.

```css
/* Radix UI */
.popover {
  transform-origin: var(--radix-popover-content-transform-origin);
}
/* Base UI */
.popover {
  transform-origin: var(--transform-origin);
}
/* Modals (always centered) */
.modal {
  transform-origin: center;
}
```

Whether the user notices individually does not matter. In the aggregate, unseen details compound.

---

## Stagger Animations

When multiple elements enter together, stagger their appearance.

```css
.item {
  opacity: 0;
  transform: translateY(8px);
  animation: fadeIn 300ms ease-out forwards;
}
.item:nth-child(1) { animation-delay: 0ms; }
.item:nth-child(2) { animation-delay: 50ms; }
.item:nth-child(3) { animation-delay: 100ms; }
.item:nth-child(4) { animation-delay: 150ms; }

@keyframes fadeIn {
  to {
    opacity: 1;
    transform: translateY(0);
  }
}
```

Keep stagger delays short (30-80ms between items). Never block interaction while stagger plays.

---

## CSS Animations (keyframes)

Use for predetermined, non-dynamic animations. Keyframes restart from zero on interruption - avoid for UI.

```css
@keyframes enter-right {
  from { opacity: 0; transform: translateX(16px); }
  to   { opacity: 1; transform: translateX(0); }
}
.enter {
  animation: enter-right 300ms ease-out forwards;
}
```

---

## Spring Animations

Springs feel more natural than duration-based animations because they simulate real physics. They maintain velocity when interrupted.

### When to use springs

- Drag interactions with momentum
- Elements that feel "alive" (Apple Dynamic Island style)
- Gestures that can be interrupted mid-animation
- Decorative mouse-tracking interactions

### Apple approach (recommended - easier to reason about)

```js
{ type: "spring", duration: 0.5, bounce: 0.2 }
```

### Traditional physics (more control)

```js
{ type: "spring", stiffness: 100, damping: 10, mass: 1 }
```

Keep bounce subtle (0.1-0.3). Avoid bounce in most UI. Use for drag-to-dismiss and playful interactions only.

### Spring-based mouse interactions

```jsx
import { useSpring } from 'framer-motion'

const springX = useSpring(mouseX * 0.1, { stiffness: 100, damping: 10 })
```

Without spring: feels artificial, instant. With spring: feels natural, has momentum.

---

## WEB ANIMATIONS API (WAAPI)

Programmatic CSS animations. Hardware-accelerated, interruptible, no library needed.

```javascript
const element = document.querySelector('.card')

const animation = element.animate([
  { transform: 'scale(1)', opacity: 1 },
  { transform: 'scale(0.95)', opacity: 0.7 },
], {
  duration: 200,
  easing: 'cubic-bezier(0.23, 1, 0.32, 1)',
  fill: 'both',
})

animation.pause()
animation.play()
animation.reverse()
animation.finish()
animation.finished.then(() => console.log('done'))
```

WAAPI beats Framer Motion under heavy load - it runs off the main thread. Use for dynamic, interruptible animations where CSS keyframes fall short.

---

## SCROLL-DRIVEN ANIMATIONS

**Browser support note:** Scroll-driven animations (`animation-timeline: view()` and `scroll()`) are Chrome/Edge-only as of 2026, not yet Baseline. Provide JS fallbacks via IntersectionObserver for Firefox/Safari.

### ViewTimeline (element enters/leaves viewport)

```css
@keyframes fade-up {
  from { opacity: 0; transform: translateY(20px); }
  to   { opacity: 1; transform: translateY(0); }
}
.reveal {
  animation: fade-up 600ms linear both;
  animation-timeline: view();
  animation-range: entry 0% entry 100%;
}
```

### ScrollTimeline (page scroll position)

```css
@keyframes shrink {
  from { height: 80px; }
  to   { height: 50px; }
}
.site-header {
  animation: shrink 300ms linear both;
  animation-timeline: scroll();
  animation-range: 0 200px;
}
```

### Scroll-linked parallax (vanilla JS)

```js
const layers = document.querySelectorAll('[data-parallax]')
window.addEventListener('scroll', () => {
  const scrollY = window.scrollY
  layers.forEach(layer => {
    const speed = parseFloat(layer.dataset.parallax) || 0.3
    layer.style.transform = `translateY(${scrollY * speed}px)`
  })
})
```

---

## FLIP Technique

For layout animations - animating elements that change position/size.

```javascript
function animateLayout(element, callback) {
  const first = element.getBoundingClientRect()
  callback()
  const last = element.getBoundingClientRect()

  const dx = first.left - last.left
  const dy = first.top - last.top
  const dw = first.width / last.width
  const dh = first.height / last.height

  element.animate([
    { transform: `translate(${dx}px, ${dy}px) scale(${dw}, ${dh})` },
    { transform: 'translate(0, 0) scale(1, 1)' },
  ], {
    duration: 300,
    easing: 'cubic-bezier(0.4, 0, 0.2, 1)',
  })
}
```

---

## CLIP-PATH for Animation

One of the most powerful animation tools in CSS. Hardware-accelerated.

```css
.overlay {
  clip-path: inset(0 100% 0 0);
  transition: clip-path 200ms ease-out;
}
.visible {
  clip-path: inset(0 0 0 0);
}
```

Use for: hold-to-delete, image reveals on scroll, comparison sliders, seamless color transitions on tabs.

---

## PERCEIVED PERFORMANCE

Speed in animation directly affects how users perceive your app:

| Effect | Mechanism |
|--------|-----------|
| Fast-spinning spinner | Makes loading feel faster (same load time, different perception) |
| 180ms select vs 400ms select | Shorter animation = perceived responsiveness jump |
| Instant tooltips after first one | Skip delay + animation on subsequent tooltips |
| Fast exit animations | Exit must be faster than enter (200ms enter, 150ms exit) |
| `ease-out` vs `ease-in` at same 200ms | ease-out feels faster because user sees immediate movement |

---

## PERFORMANCE RULES

### Only animate transform and opacity

These skip layout and paint, running on GPU.

### CSS variables are inheritable - avoid for animation

```js
// Bad: triggers recalculation on all children
element.style.setProperty('--swipe-amount', `${distance}px`)

// Good: only affects this element
element.style.transform = `translateY(${distance}px)`
```

### CSS animations beat JS under load

When the browser is busy loading a new page, Framer Motion animations (using `requestAnimationFrame`) drop frames. CSS animations remain smooth - they run off the main thread.

### Framer Motion: use `transform` string, not shorthand

```jsx
// NOT hardware accelerated (requestAnimationFrame on main thread)

// Hardware accelerated (GPU, stays smooth even under load)

```

### will-change: use sparingly

```css
.will-animate {
  will-change: transform, opacity;
}
```

Each `will-change` creates a new compositor layer, consuming GPU memory. Apply before animation, remove after.

---

## ACCESSIBILITY

```css
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}
```

WCAG 2.1 Success Criterion 2.3.3 - Animation from Interactions.

For fine-grained control:

```javascript
const prefersReduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches
const userPrefersReduced = localStorage.getItem('reduced-motion') === 'true'
co

…

## Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [EliasOulkadi](https://github.com/EliasOulkadi)
- **Source:** [EliasOulkadi/shokunin](https://github.com/EliasOulkadi/shokunin)
- **License:** MIT
- **Homepage:** https://eliasoulkadi.github.io/shokunin/

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** no
- **Filesystem access:** no
- **Shell / process execution:** no
- **Environment & secrets:** no
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/skill-eliasoulkadi-shokunin-motion-craft
- Seller: https://agentstack.voostack.com/s/eliasoulkadi
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
