hermes-hub/.agents/skills/frontend-design/motion.md

293 lines
9.6 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Motion — Restraint, Intent, Feel
> Animation is feedback, not decoration. Every motion must communicate: state changed, content arrived, attention needed. If it doesn't communicate — remove it.
---
## The Three Questions
Before adding any animation, ask:
1. **What does this motion communicate?** ("The button is now active" / "This content is new" / "Loading finished")
2. **What happens if I remove it?** (Usually: nothing — and that's the test)
3. **Is it accessible?** (Does it respect `prefers-reduced-motion`?)
If you can't answer #1, delete it.
---
## Principles
### 1. Less motion, more meaning
A page with 12 different entrance animations feels unstable. A page with ONE entrance system feels considered.
**Pick one entrance system. Pick one hover system. Pick one page-transition pattern. Use them throughout.**
### 2. Easing is everything
- **`ease-out`** for things arriving (decals landing on screen, modals opening, content appearing)
- **`ease-in`** for things leaving (modals closing, content dismissed)
- **`ease-in-out`** for things that loop or oscillate
- **`linear`** for things that are continuous and infinite (rare — loading spinners)
- **Custom cubic-bezier** for character: `cubic-bezier(0.32, 0.72, 0, 1)` (Apple-style, "expressive out") or `cubic-bezier(0.4, 0, 0.2, 1)` (Material standard)
**Avoid:** `ease` (default — the default is rarely the right answer for important moments).
### 3. Duration is the dial
Faster = more responsive. Slower = more dramatic.
| Type | Duration |
|---|---|
| Hover state change | 80150ms |
| Button press | 60100ms |
| Modal open | 150250ms |
| Modal close | 100200ms |
| Tooltip appear | 100150ms |
| Content fade in | 200400ms |
| Page transition | 250500ms |
| Hero entrance | 500800ms (one moment — not every section) |
| Skeleton shimmer loop | 1500ms |
| Marquee / infinite scroll | 3000060000ms (very slow) |
**Rule of thumb:** the smaller the change, the faster the transition. The bigger the change, the longer it can take.
### 4. Distance is small
Things should move **a little**. A modal opening from `scale(0.9) → scale(1)` (10% growth) feels elegant. From `scale(0.5) → scale(1)` (50% growth) feels cartoonish.
**Default:** content shifts 412px. Modals scale 0.961. Cards lift 24px. Hover scale 1.021.05 max.
---
## Entrance Animations
### The system
Choose ONE entrance pattern. Apply to:
- Hero elements (on load)
- Content sections (on scroll into view)
- Modal/dialog content
- Toast notifications
**Options:**
**Fade** — most subtle, almost universal
```css
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.fade-in {
animation: fadeIn 400ms ease-out both;
}
```
**Fade + rise** — slightly more dramatic, good for text
```css
@keyframes fadeRise {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: translateY(0); }
}
```
**Fade + slide** — for sidebar items, list items
```css
@keyframes fadeSlide {
from { opacity: 0; transform: translateX(-12px); }
to { opacity: 1; transform: translateX(0); }
}
```
**Stagger** — for groups of items (lists, grids)
```css
.stagger-item { opacity: 0; animation: fadeRise 400ms ease-out forwards; }
.stagger-item:nth-child(1) { animation-delay: 0ms; }
.stagger-item:nth-child(2) { animation-delay: 60ms; }
.stagger-item:nth-child(3) { animation-delay: 120ms; }
.stagger-item:nth-child(4) { animation-delay: 180ms; }
/* etc — or use CSS variables for delay */
```
### Entrance anti-patterns
- ❌ Every section animating in as you scroll (exhausting)
- ❌ Long durations (1s+) for routine content
- ❌ Bouncy easing (`cubic-bezier(0.68, -0.55, 0.265, 1.55)`) on serious interfaces
- ❌ Slide-in from random directions (left, right, top, bottom — pick one)
- ❌ Different animation types per section (no system)
---
## Hover Animations
### The system
Pick a hover pattern. Apply to all interactive elements of a kind.
**Buttons**
- Background color change: 120ms
- Optional: subtle scale `transform: scale(1.02)` — only on prominent CTAs
**Cards**
- Border color strengthens OR background tints slightly OR 2px lift via translateY
- Pick ONE. Don't combine.
**Links**
- Underline grows from left (preferred) OR color change
- 150ms
**Icons**
- Slight rotate (510deg) OR slight scale (1.1)
- Pick the right direction for the meaning (e.g., arrow rotates forward, chevron rotates down)
### Hover anti-patterns
-`transform: scale(1.1)` on every hover — feels unstable
- ❌ Color shifts that don't match the palette (random bright colors)
- ❌ Multiple property changes at once (color + size + shadow + rotate)
- ❌ Long durations on hover (anything > 200ms feels laggy)
---
## Scroll-triggered Animations
**Default:** don't animate on scroll. Content appears when it appears.
**When scroll animations ARE appropriate:**
- Long-form editorial pages (sections reveal as you read)
- Image galleries (lazy reveal as you scroll)
- Data visualizations (animate in as they enter viewport)
- Storytelling / product tours
**How to do it well:**
- Trigger once (`IntersectionObserver`, threshold 0.10.2)
- Animation is subtle (fade + small rise)
- Don't replay on scroll back
- Provide a no-JS fallback (content visible by default)
```js
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add('in-view');
observer.unobserve(entry.target);
}
});
}, { threshold: 0.15 });
document.querySelectorAll('.reveal').forEach(el => observer.observe(el));
```
```css
.reveal {
opacity: 0;
transform: translateY(12px);
transition: opacity 600ms ease-out, transform 600ms ease-out;
}
.reveal.in-view {
opacity: 1;
transform: translateY(0);
}
```
### Scroll animation anti-patterns
- ❌ Parallax everywhere (rarely adds value)
- ❌ Horizontal scroll as the only way to navigate a section
- ❌ Content invisible until JS loads (broken without JS)
- ❌ Replaying animations on every scroll back through
- ❌ "Scroll to discover" with no clear signal of what comes next
---
## Page Transitions
For SPAs and multi-page sites with shared chrome.
**The rule:** fast, consistent, and barely noticeable.
- **Fade transition:** 200ms cross-fade between pages
- **Slide (subtle):** outgoing content slides 20px left, incoming slides in from right — only if the navigation is forward/back in a clear sequence
- **Duration:** 200300ms max
**Avoid:**
- ❌ Heavy transitions that delay content (users notice delay as "broken")
- ❌ Different transition styles for different navigation actions
- ❌ Animated logos or brand marks on every page load
---
## Micro-interactions Worth Their Weight
- **Toggle switches** — smooth slide with color change
- **Checkbox check** — satisfying tick animation
- **Drag handles** — feedback as user drags
- **Form validation** — color change + small shake on error (subtle, not aggressive)
- **Toast notifications** — slide in from corner, auto-dismiss with progress bar
- **Number counters** — counting up to value (data viz, hero stats)
- **Progress bars** — width transitions smoothly, not jumps
- **Loading completion** — content fades in smoothly, skeleton → real
---
## Performance
### Animate these properties (cheap, GPU-accelerated):
- `transform` (translate, scale, rotate)
- `opacity`
### Avoid animating these (expensive, layout-thrashing):
- `width`, `height`
- `top`, `left`, `right`, `bottom`
- `margin`, `padding`
- `border-width`
- `box-shadow` (acceptable for small elements; expensive for large)
### Tips
- Use `will-change: transform` sparingly (only on elements about to animate)
- Use `transform: translateZ(0)` or `will-change` to promote to GPU layer
- Use `requestAnimationFrame` for JS animations
- For long-running animations, use `transform` and `opacity` only
---
## Accessibility — `prefers-reduced-motion`
**Required.** Some users get nauseous, dizzy, or worse from motion. Respect their setting.
```css
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}
```
Or be more selective — only kill the heavy stuff:
```css
@media (prefers-reduced-motion: reduce) {
.reveal { opacity: 1; transform: none; transition: none; }
.parallax { transform: none !important; }
}
```
**Always test:** toggle "Reduce motion" in your OS settings. Visit the site. Does it still work? Is content still visible?
---
## Motion Anti-Patterns
| ❌ Don't | ✅ Do |
|---|---|
| Animate every section on scroll | Animate selectively, or none |
| `transition: all` on every element | Specify which properties animate |
| Bouncy spring physics on every UI | Most UI should be linear-feeling ease-out |
| Parallax on every image | Parallax only when it adds to the narrative |
| Long page transitions (>400ms) | Keep page transitions under 300ms |
| `infinite` animations | Animations should have a clear end |
| Hover scale of 1.1+ | Subtle scale (1.021.05) if any |
| Different motion styles in different sections | One system, applied consistently |
| Animations that require JS to see content | Content visible by default; animations enhance |
| Skipping `prefers-reduced-motion` | Always honor it |
| Animated gradient backgrounds | Static backgrounds or no backgrounds |
| Marquee text scrolling fast | Slow, considered (or don't) |
| Number counting from 0 with 4s duration | Count quickly (12s) or show the final value |
| Loading spinner that takes 30s | Show progress, not just a spinner |
| Toast that bounces in | Slide in, fade out |