Automattic logo

Skill

motion-and-micro-interactions

apply premium motion and micro-interactions

Covers Interaction Animation Frontend Design

Description

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.md

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:

/* 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:

DurationUse caseSource
75msButton press/active stateAutosend (ease-in duration-75)
100-150msHover color swaps, icon arrowsReducto, Antimetal (--default-transition-duration: .15s)
200msCard hover, background shiftsCal, Incident, most sites
250msButton color + ring deltaHex, Incident
300msColor-only transitionsReducto transition-colors duration-300
500msScroll-triggered reveals, parallaxAmplemarket
1500msSlow 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):

.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:

.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:

.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:

.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.

.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:

.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:

.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 hovershadow-lgshadow-2xl:

.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:

.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;
}
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:

.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:

/* 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); }
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

: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

PatternCase study
cubic-bezier(.215,.61,.355,1) easeOutCubicAmplemarket
75ms snappy button transitionsAutosend
1.5s slow opacity fadeCohere
Inset-inversion press feedbackAmplemarket
Width-animated buttonCohere (max-width)
Orchestrated child translation on hoverAntimetal
Blur-in hero revealAntimetal (blur-[4px] → 0)
Shadow-grow card hoverCal, Hex, Incident
Mouse-tracking gradient (CSS var)Amplemarket (WebGL), Shade (cursor spotlight)
Conic-gradient halo spinShade
Scroll-linked per-section timelinesAngelList (ScrollTrackingWrapper)
Kbd keycap 3D shadowIncident (box-shadow: 0 3px 0)

© 2026 YourAI.tools. Every skill from an identity-verified publisher.

Independent catalog. Not affiliated with, endorsed by, or sponsored by Anthropic or any listed publisher. All trademarks belong to their respective owners.