| name | motion-and-micro-interactions |
| 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. |
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:
--ease-amplemarket: cubic-bezier(.215, .61, .355, 1);
--ease-material: cubic-bezier(.4, 0, .2, 1);
--ease-hex: cubic-bezier(.25, .1, .25, 1);
--ease-out-tw: cubic-bezier(0, 0, .2, 1);
--ease-framer: cubic-bezier(.44, 0, .56, 1);
--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):
.btn {
transition: all 75ms ease-in;
}
.btn:active {
box-shadow: none;
transform: scale(0.95);
}
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,
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,
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%; }
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);
}
.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 hover — shadow-lg → shadow-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 {
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);
}
.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 {
--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);
--duration-press: 75ms;
--duration-quick: 150ms;
--duration-base: 200ms;
--duration-reveal: 600ms;
--duration-slow: 1500ms;
}
.btn {
transition: all var(--duration-press) ease-in;
}
.btn:hover {
transition-duration: var(--duration-base);
}
.btn:active {
transform: scale(0.97);
}
.card {
transition: box-shadow var(--duration-base) var(--ease-standard);
}
@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 {
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);
}
.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
- Replace every
ease-in-out with one of the 4 named cubic-beziers above.
- Set duration by interaction type: 75ms active, 150-200ms hover, 600ms reveal.
- Give buttons an
:active state with scale(0.95) or inset-inversion.
- Hover cards via shadow, not translateY.
- Add blur-in reveals to hero text lines with staggered delays.
- Add IntersectionObserver section reveals with 80ms stagger between children.
- 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) |