# Motion And Micro Interactions

> Apply the motion language of premium VC-funded sites — specific cubic-bezier easings (not ease-in-out), snappy 75-200ms durations, mechanical press feedback, orchestrated hover deltas, width-animated buttons, mouse-tracking gradients, blur-in reveals, scroll-linked reveals. Use when interactions feel "springy and generic" or when the user wants their site to feel responsive, alive, physical.

- Skill: `automattic/motion-and-micro-interactions` (Agent Skill)
- Install (CLI): `npx skillmds@latest add automattic/motion-and-micro-interactions`
- Raw SKILL.md: https://api.skillmd.com/api/skills/automattic/motion-and-micro-interactions/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: Automattic (https://skillmd.com/u/automattic)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/automattic/motion-and-micro-interactions

---


# Motion and Micro-Interactions

Premium sites don't use "ease-in-out 300ms" — they use specific easings, deliberate durations, orchestrated hover states, and physical press feedback. Extracted from Amplemarket, Cohere, Antimetal, Hex, Autosend, Shade, Cal, Amplemarket, AngelList.

## When to invoke

- Transitions use `transition: all 300ms ease-in-out` defaults
- Buttons have `hover:bg-blue-600` only — no press, no scale, no delta beyond color
- Scroll doesn't do anything
- Cursor feels static on the page
- User says design feels "flat," "lifeless," "not responsive"

## The 7 core rules

### Rule 1: Stop using `ease-in-out` — pick a specific cubic-bezier

Case-study sites use **specific, named cubic-bezier curves**, not browser defaults:

```css
/* Amplemarket's signature easeOutCubic — snappy entry, soft settle */
--ease-amplemarket: cubic-bezier(.215, .61, .355, 1);

/* Material standard — used by Cohere, Antimetal, many others */
--ease-material: cubic-bezier(.4, 0, .2, 1);

/* Hex's explicit default — just the CSS default made intentional */
--ease-hex: cubic-bezier(.25, .1, .25, 1);

/* Tailwind-style ease-out — punchy, used by Hex, Shade */
--ease-out-tw: cubic-bezier(0, 0, .2, 1);

/* Framer spring default (Shade uses this) */
--ease-framer: cubic-bezier(.44, 0, .56, 1);

/* Reducto's in-out pair */
--ease-in-out-reducto: cubic-bezier(.4, 0, .2, 1);
--ease-out-reducto: cubic-bezier(0, 0, .2, 1);
```

**Rule of thumb**: use `cubic-bezier(.4, 0, .2, 1)` as your default. Use `cubic-bezier(.215, .61, .355, 1)` for "pop-in" entrances. Use `cubic-bezier(0, 0, .2, 1)` for anything that needs to feel snappy.

Anti-pattern: `transition: all 0.3s ease-in-out;` everywhere.

### Rule 2: Durations matter — 75ms is tactile, 200ms is calm, 1500ms is editorial

The default `300ms` is lazy. Premium sites pick specific durations per interaction type:

| Duration | Use case | Source |
|---|---|---|
| 75ms | Button press/active state | Autosend (`ease-in duration-75`) |
| 100-150ms | Hover color swaps, icon arrows | Reducto, Antimetal (`--default-transition-duration: .15s`) |
| 200ms | Card hover, background shifts | Cal, Incident, most sites |
| 250ms | Button color + ring delta | Hex, Incident |
| 300ms | Color-only transitions | Reducto `transition-colors duration-300` |
| 500ms | Scroll-triggered reveals, parallax | Amplemarket |
| 1500ms | Slow opacity fade-in (editorial) | Cohere (`opacity 1.5s ease-in`) |

**Button press should be FAST (75-100ms)**. Hover deltas can be 200ms. Scroll reveals can breathe at 400-600ms.

### Rule 3: Buttons need mechanical press feedback

`hover:bg-blue-600` is not enough. Give the button a **physical pressed state**.

**Autosend's mechanical press** (copy this):
```css
.btn {
  transition: all 75ms ease-in;
}
.btn:active {
  box-shadow: none;          /* drops the lift */
  transform: scale(0.95);    /* 5% compression */
}
```

**Amplemarket's inset-inversion press** — swaps the inset highlight from white to dark so the button LOOKS pushed in:
```css
.btn {
  box-shadow:
    0 6px 2px #11111103,
    0 3px 2px #1111110D,
    0 1px 1px #11111117,
    0 0 1px #1111111f,
    inset 0 6px 12px #ffffff33,      /* ← highlight */
    inset 0 1px 1px #fff3;
}
.btn:active {
  box-shadow:
    0 6px 2px #11111103,
    0 3px 2px #1111110D,
    0 1px 1px #11111117,
    0 0 1px #1111111f,
    inset 0 6px 12px #11111133,      /* ← inverts to dark */
    inset 0 1px 1px #111111f3;
}
```

**Antimetal's orchestrated hover** — sub-elements physically translate apart:
```css
.cta-button:hover .cta-label { transform: translateX(-36px); }
.cta-button:hover .cta-arrow { transform: translateX(36px); }
.cta-button:hover { width: 102%; }  /* tiny scale-x too */
```

**Cohere's width-animated button** — the button itself expands on hover:
```css
.primary-btn {
  max-width: 200px;
  transition: max-width .2s ease-in-out;
}
.primary-btn:hover { max-width: 300px; }
```

Anti-pattern: `.btn:hover { background: darker; }` (color-only delta).

### Rule 4: Hero text reveals with blur-in, not opacity fade

Standard AI reveal: `opacity: 0 → opacity: 1` (boring).
Antimetal's reveal: vertical rise + blur clearing + opacity.

```css
.hero-line {
  opacity: 0;
  transform: translateY(48px);
  filter: blur(4px);
  transition: opacity 600ms, transform 600ms, filter 600ms;
  transition-timing-function: cubic-bezier(.215, .61, .355, 1);
}
.hero-line.visible {
  opacity: 1;
  transform: translateY(0);
  filter: blur(0);
}

/* Stagger multiple lines */
.hero-line:nth-child(1) { transition-delay: 0ms; }
.hero-line:nth-child(2) { transition-delay: 100ms; }
.hero-line:nth-child(3) { transition-delay: 200ms; }
```

### Rule 5: Cards hover with shadow growth, not translateY

Most sites don't lift cards — they **grow the shadow + ring weight**:

**Cal's hover** — ring darkens from 8% to 10% alpha:
```css
.card {
  box-shadow:
    0px 1px 5px -4px rgba(19,19,22,0.7),
    0px 0px 0px 1px rgba(34,42,53,0.08),
    0px 4px 8px 0px rgba(34,42,53,0.05);
  transition: box-shadow 200ms;
}
.card:hover {
  box-shadow:
    0px 1px 5px -4px rgba(19,19,22,0.7),
    0px 0px 0px 1px rgba(34,42,53,0.10),
    0px 4px 8px 0px rgba(34,42,53,0.05);
}
```

**Hex's hover** — ring weight and contact shadow deepen:
```css
.card {
  box-shadow:
    0 0 0 1px rgba(49,38,59,0.22),
    0 103px 103px 0 rgba(49,38,59,0.09),
    0 26px 57px 0 rgba(49,38,59,0.10);
}
.card:hover {
  box-shadow:
    0 0 0 1px rgba(49,38,59,0.32),
    0 103px 103px 0 rgba(49,38,59,0.12),
    0 26px 57px 0 rgba(49,38,59,0.15);
}
```

**Incident's hover** — `shadow-lg` → `shadow-2xl`:
```css
.card { box-shadow: var(--tw-shadow-lg); transition: box-shadow 250ms; }
.card:hover { box-shadow: var(--tw-shadow-2xl); }
```

Anti-pattern: `.card:hover { transform: translateY(-4px); }` (generic).

### Rule 6: Mouse-tracking effects create living pages

**Amplemarket** — the hero text is a `radial-gradient` whose position follows the mouse via CSS variables:

```css
.hero-text {
  background: radial-gradient(
    80% 109% at var(--gradient-posX, 50%) var(--gradient-posY, 50%),
    #D0B2FF 0%,
    #FFEED8 50%,
    #E8400D 100%);
  background-clip: text;
  color: transparent;
}
```

```js
document.addEventListener('mousemove', (e) => {
  const { clientX, clientY, currentTarget } = e;
  const { width, height } = currentTarget.getBoundingClientRect();
  document.documentElement.style.setProperty('--gradient-posX', `${(clientX/width)*100}%`);
  document.documentElement.style.setProperty('--gradient-posY', `${(clientY/height)*100}%`);
});
```

**Shade** — a chartreuse (#c6eb57) spotlight follows the cursor over the hero. Plus a `conic-gradient` halo that spins around the AI search lozenge:

```css
.ai-halo {
  background: conic-gradient(transparent 200deg, rgb(133, 92, 247));
  animation: spin 2s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
```

### Rule 7: Scroll-triggered reveals per section (not per element)

Premium sites reveal content in **staggered groups** with a subtle delta, not per-element fades:

```css
/* Section reveals as a group on scroll into view */
.section {
  opacity: 0;
  transform: translateY(24px);
  transition: opacity 600ms cubic-bezier(.215,.61,.355,1) 0ms,
              transform 600ms cubic-bezier(.215,.61,.355,1) 0ms;
}
.section.in-view {
  opacity: 1;
  transform: translateY(0);
}

/* Children stagger by nth-child delay */
.section .reveal-item { transition-delay: calc(var(--index, 0) * 80ms); }
```

```js
const observer = new IntersectionObserver((entries) => {
  entries.forEach((entry) => {
    if (entry.isIntersecting) entry.target.classList.add('in-view');
  });
}, { threshold: 0.15 });
document.querySelectorAll('.section').forEach((el) => observer.observe(el));
```

**AngelList** goes further with scroll-linked parallax and pinned sections — `ScrollTrackingWrapper` drives per-section reveals with 2-5 second timelines.

**Incident's hero** rotates through 5 product screenshots via Swiper — the real product UI is the motion.

## Complete motion system starter

```css
:root {
  /* Easings */
  --ease-snappy: cubic-bezier(.215, .61, .355, 1);
  --ease-standard: cubic-bezier(.4, 0, .2, 1);
  --ease-out: cubic-bezier(0, 0, .2, 1);
  --ease-framer: cubic-bezier(.44, 0, .56, 1);

  /* Durations */
  --duration-press: 75ms;
  --duration-quick: 150ms;
  --duration-base: 200ms;
  --duration-reveal: 600ms;
  --duration-slow: 1500ms;
}

/* Buttons */
.btn {
  transition: all var(--duration-press) ease-in;
}
.btn:hover {
  transition-duration: var(--duration-base);
}
.btn:active {
  transform: scale(0.97);
}

/* Cards — shadow hover, not lift */
.card {
  transition: box-shadow var(--duration-base) var(--ease-standard);
}

/* Hero reveal */
@keyframes reveal-up {
  from { opacity: 0; transform: translateY(48px); filter: blur(4px); }
  to   { opacity: 1; transform: translateY(0); filter: blur(0); }
}
.hero-line {
  animation: reveal-up var(--duration-reveal) var(--ease-snappy) both;
}
.hero-line:nth-child(1) { animation-delay: 0ms; }
.hero-line:nth-child(2) { animation-delay: 100ms; }
.hero-line:nth-child(3) { animation-delay: 200ms; }

/* Section scroll reveal */
.section {
  opacity: 0;
  transform: translateY(24px);
  transition: opacity var(--duration-reveal) var(--ease-snappy),
              transform var(--duration-reveal) var(--ease-snappy);
}
.section.in-view {
  opacity: 1;
  transform: translateY(0);
}

/* Cursor spotlight (Shade-style) */
.hero-spotlight {
  background: radial-gradient(
    250px circle at var(--mx, 50%) var(--my, 50%),
    rgba(198, 235, 87, 0.4),
    transparent 40%);
  pointer-events: none;
}
```

## Application workflow

1. **Replace every `ease-in-out`** with one of the 4 named cubic-beziers above.
2. **Set duration by interaction type**: 75ms active, 150-200ms hover, 600ms reveal.
3. **Give buttons an `:active` state** with `scale(0.95)` or inset-inversion.
4. **Hover cards via shadow**, not translateY.
5. **Add blur-in reveals** to hero text lines with staggered delays.
6. **Add IntersectionObserver section reveals** with 80ms stagger between children.
7. **Consider a mouse-tracking effect** for the hero (spotlight, gradient position, or tilt).

## Anti-patterns to kill

- `transition: all 300ms ease-in-out` on everything
- `.btn:hover { background-color: darker; }` — color-only hover with no press
- Cards that `translateY(-4px)` on hover (cliché)
- `opacity: 0 → 1` fades with no translateY or blur
- Transitions that animate `all` properties (perf + visual noise)
- Hero animations that don't stagger
- Missing `:active` state on buttons (breaks physical feel)
- Animating `transform: translateX(...)` with a generic 300ms ease

## Site references

| Pattern | Case study |
|---|---|
| cubic-bezier(.215,.61,.355,1) easeOutCubic | Amplemarket |
| 75ms snappy button transitions | Autosend |
| 1.5s slow opacity fade | Cohere |
| Inset-inversion press feedback | Amplemarket |
| Width-animated button | Cohere (max-width) |
| Orchestrated child translation on hover | Antimetal |
| Blur-in hero reveal | Antimetal (`blur-[4px]` → 0) |
| Shadow-grow card hover | Cal, Hex, Incident |
| Mouse-tracking gradient (CSS var) | Amplemarket (WebGL), Shade (cursor spotlight) |
| Conic-gradient halo spin | Shade |
| Scroll-linked per-section timelines | AngelList (ScrollTrackingWrapper) |
| Kbd keycap 3D shadow | Incident (`box-shadow: 0 3px 0`) |

