@champpaba/claude-agent-kit 1.4.0 → 1.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/agents/02-uxui-frontend.md +111 -2
- package/.claude/commands/pageplan.md +369 -9
- package/.claude/contexts/design/box-thinking.md +358 -0
- package/.claude/contexts/patterns/animation-patterns.md +768 -0
- package/.claude/contexts/patterns/performance-optimization.md +421 -0
- package/.claude/contexts/patterns/ui-component-consistency.md +49 -2
- package/.claude/lib/agent-executor.md +69 -10
- package/.claude/templates/page-plan-example.md +131 -0
- package/LICENSE +21 -21
- package/README.md +1278 -1179
- package/bin/cli.js +59 -61
- package/lib/helpers.js +144 -0
- package/lib/init.js +57 -52
- package/lib/update.js +90 -73
- package/package.json +1 -1
|
@@ -0,0 +1,768 @@
|
|
|
1
|
+
# Animation & Micro-interaction Patterns
|
|
2
|
+
|
|
3
|
+
> **Purpose:** Add life and polish to UI through thoughtful animations and interactions
|
|
4
|
+
> **Audience:** uxui-frontend agent (primary), all frontend agents
|
|
5
|
+
> **Integration:** Used with `/pageplan` Section 2.6 (Animation Blueprint)
|
|
6
|
+
> **Philosophy:** Match Flow Engineer Step 3 - Design animations systematically
|
|
7
|
+
|
|
8
|
+
**Key Principles:**
|
|
9
|
+
- **Purpose:** Every animation should have a purpose (guide attention, provide feedback, show transition)
|
|
10
|
+
- **Performance:** Use `transform` and `opacity` only (GPU-accelerated)
|
|
11
|
+
- **Accessibility:** Respect `prefers-reduced-motion` for users sensitive to motion
|
|
12
|
+
- **Subtlety:** Animations should enhance, not distract
|
|
13
|
+
- **Consistency:** Same component type = same animation pattern
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## 📘 How to Use This File
|
|
18
|
+
|
|
19
|
+
**Priority:**
|
|
20
|
+
1. **Page-specific plan:** `.changes/{id}/page-plan.md` Section 2.6 (if exists)
|
|
21
|
+
2. **Project tokens:** `design-system/STYLE_TOKENS.json` (animation tokens)
|
|
22
|
+
3. **General guidelines:** This file (fallback when above don't exist)
|
|
23
|
+
|
|
24
|
+
**When to read:**
|
|
25
|
+
- uxui-frontend agent STEP 0 (after loading project context)
|
|
26
|
+
- When creating components with interactions
|
|
27
|
+
- When no page-plan.md Animation Blueprint exists
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## ⚡ Animation Performance Rules
|
|
32
|
+
|
|
33
|
+
### ✅ Fast Properties (GPU-accelerated)
|
|
34
|
+
|
|
35
|
+
**Use these properties for smooth 60fps animations:**
|
|
36
|
+
|
|
37
|
+
```css
|
|
38
|
+
/* ✅ GOOD - GPU accelerated */
|
|
39
|
+
.element {
|
|
40
|
+
transform: translateX(100px); /* Position */
|
|
41
|
+
transform: scale(1.1); /* Size */
|
|
42
|
+
transform: rotate(45deg); /* Rotation */
|
|
43
|
+
opacity: 0.5; /* Transparency */
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### ❌ Slow Properties (Avoid)
|
|
48
|
+
|
|
49
|
+
**These trigger layout recalculation - use sparingly:**
|
|
50
|
+
|
|
51
|
+
```css
|
|
52
|
+
/* ❌ BAD - Causes reflow/repaint */
|
|
53
|
+
.element {
|
|
54
|
+
width: 200px; /* Triggers layout */
|
|
55
|
+
height: 100px; /* Triggers layout */
|
|
56
|
+
top: 50px; /* Triggers layout */
|
|
57
|
+
left: 100px; /* Triggers layout */
|
|
58
|
+
margin: 20px; /* Triggers layout */
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
**Source:** FreeCodeCamp Performance Handbook
|
|
63
|
+
- `transform`/`opacity`: ~3ms render time
|
|
64
|
+
- Layout properties: ~14ms render time (4-5× slower)
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## 🎯 Animation Duration Guidelines
|
|
69
|
+
|
|
70
|
+
**Standard Durations (from STYLE_TOKENS.json):**
|
|
71
|
+
|
|
72
|
+
| Token | Duration | Use Case | Example |
|
|
73
|
+
|-------|----------|----------|---------|
|
|
74
|
+
| **fast** | 150ms | Micro-interactions | Button hover, icon rotation |
|
|
75
|
+
| **normal** | 300ms | Transitions, reveals | Card elevation, modal entrance |
|
|
76
|
+
| **slow** | 500ms | Complex animations | Page transitions, carousel |
|
|
77
|
+
|
|
78
|
+
**❌ DON'T use random durations:** 200ms, 250ms, 400ms (inconsistent)
|
|
79
|
+
**✅ DO use token durations:** 150ms, 300ms, 500ms (consistent, predictable)
|
|
80
|
+
|
|
81
|
+
```css
|
|
82
|
+
/* Fast (hover effects) */
|
|
83
|
+
.button:hover {
|
|
84
|
+
transform: translateY(-2px);
|
|
85
|
+
transition: transform 150ms ease-out;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/* Normal (modal entrance) */
|
|
89
|
+
.modal {
|
|
90
|
+
animation: fadeIn 300ms ease-out;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/* Slow (page transition) */
|
|
94
|
+
.page-transition {
|
|
95
|
+
animation: slideIn 500ms ease-in-out;
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## 🧩 Component Animation Patterns
|
|
102
|
+
|
|
103
|
+
> **Purpose:** Standard animation patterns for common components
|
|
104
|
+
> **Source:** Based on STYLE_TOKENS.json + `/pageplan` Section 2.6
|
|
105
|
+
> **Principle:** Same component type = same animation pattern (consistency)
|
|
106
|
+
|
|
107
|
+
### Button Components
|
|
108
|
+
|
|
109
|
+
#### Pattern: Scale + Shadow (Primary/CTA)
|
|
110
|
+
```tsx
|
|
111
|
+
<button className="
|
|
112
|
+
transition-all duration-150
|
|
113
|
+
hover:scale-105 hover:shadow-lg
|
|
114
|
+
active:scale-95
|
|
115
|
+
">
|
|
116
|
+
Get Started
|
|
117
|
+
</button>
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
**States:**
|
|
121
|
+
- **Hover:** Scale(1.05) + Shadow(md→lg) | 150ms
|
|
122
|
+
- **Active:** Scale(0.95) | 100ms (immediate feedback)
|
|
123
|
+
- **Disabled:** Opacity(70%) | static
|
|
124
|
+
|
|
125
|
+
**Rationale:** Creates depth, confirms interactivity, tactile press feedback
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
#### Pattern: Background Shift (Secondary)
|
|
130
|
+
```tsx
|
|
131
|
+
<button className="
|
|
132
|
+
transition-colors duration-150
|
|
133
|
+
hover:bg-secondary/80
|
|
134
|
+
">
|
|
135
|
+
Learn More
|
|
136
|
+
</button>
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
**Rationale:** Subtle, doesn't distract from primary CTA
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
### Card Components
|
|
144
|
+
|
|
145
|
+
#### Pattern: Shadow Elevation
|
|
146
|
+
```tsx
|
|
147
|
+
<div className="
|
|
148
|
+
transition-shadow duration-300
|
|
149
|
+
hover:shadow-xl hover:border-primary/50
|
|
150
|
+
">
|
|
151
|
+
{/* Card content */}
|
|
152
|
+
</div>
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
**States:**
|
|
156
|
+
- **Hover:** Shadow(sm→xl) + Border glow | 300ms
|
|
157
|
+
- **Click (if interactive):** Scale(0.98) | 100ms
|
|
158
|
+
|
|
159
|
+
**Rationale:** Elegant, smooth, matches Material Design
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
### Input & Form Components
|
|
164
|
+
|
|
165
|
+
#### Pattern: Ring + Border Shift
|
|
166
|
+
```tsx
|
|
167
|
+
<input className="
|
|
168
|
+
transition-all duration-200
|
|
169
|
+
focus:ring-2 focus:ring-primary focus:border-primary
|
|
170
|
+
" />
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
**States:**
|
|
174
|
+
- **Focus:** Ring(2px primary) + Border(primary) | 200ms
|
|
175
|
+
- **Error:** Border(destructive) + optional shake | 300ms
|
|
176
|
+
|
|
177
|
+
**Rationale:** Clear focus indicator (accessibility), balanced duration
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
|
|
181
|
+
## 🎨 Entrance Animations
|
|
182
|
+
|
|
183
|
+
### 1. Fade In
|
|
184
|
+
|
|
185
|
+
**Use for:** Images, cards, content sections
|
|
186
|
+
|
|
187
|
+
```css
|
|
188
|
+
@keyframes fadeIn {
|
|
189
|
+
from {
|
|
190
|
+
opacity: 0;
|
|
191
|
+
}
|
|
192
|
+
to {
|
|
193
|
+
opacity: 1;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
.fade-in {
|
|
198
|
+
animation: fadeIn 300ms ease-out;
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
**React/Next.js:**
|
|
203
|
+
```tsx
|
|
204
|
+
<div className="animate-fadeIn opacity-0">
|
|
205
|
+
Content appears smoothly
|
|
206
|
+
</div>
|
|
207
|
+
|
|
208
|
+
// Tailwind config
|
|
209
|
+
module.exports = {
|
|
210
|
+
theme: {
|
|
211
|
+
extend: {
|
|
212
|
+
keyframes: {
|
|
213
|
+
fadeIn: {
|
|
214
|
+
'0%': { opacity: '0' },
|
|
215
|
+
'100%': { opacity: '1' },
|
|
216
|
+
},
|
|
217
|
+
},
|
|
218
|
+
animation: {
|
|
219
|
+
fadeIn: 'fadeIn 300ms ease-out forwards',
|
|
220
|
+
},
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
}
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
---
|
|
227
|
+
|
|
228
|
+
### 2. Slide In
|
|
229
|
+
|
|
230
|
+
**Use for:** Sidebars, notifications, modals
|
|
231
|
+
|
|
232
|
+
```css
|
|
233
|
+
/* Slide from bottom */
|
|
234
|
+
@keyframes slideInBottom {
|
|
235
|
+
from {
|
|
236
|
+
opacity: 0;
|
|
237
|
+
transform: translateY(20px);
|
|
238
|
+
}
|
|
239
|
+
to {
|
|
240
|
+
opacity: 1;
|
|
241
|
+
transform: translateY(0);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
.slide-in-bottom {
|
|
246
|
+
animation: slideInBottom 300ms ease-out;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/* Slide from right (sidebar) */
|
|
250
|
+
@keyframes slideInRight {
|
|
251
|
+
from {
|
|
252
|
+
transform: translateX(100%);
|
|
253
|
+
}
|
|
254
|
+
to {
|
|
255
|
+
transform: translateX(0);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
.sidebar {
|
|
260
|
+
animation: slideInRight 250ms ease-out;
|
|
261
|
+
}
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
---
|
|
265
|
+
|
|
266
|
+
### 3. Scale In
|
|
267
|
+
|
|
268
|
+
**Use for:** Modals, tooltips, popovers
|
|
269
|
+
|
|
270
|
+
```css
|
|
271
|
+
@keyframes scaleIn {
|
|
272
|
+
from {
|
|
273
|
+
opacity: 0;
|
|
274
|
+
transform: scale(0.95);
|
|
275
|
+
}
|
|
276
|
+
to {
|
|
277
|
+
opacity: 1;
|
|
278
|
+
transform: scale(1);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
.modal {
|
|
283
|
+
animation: scaleIn 200ms ease-out;
|
|
284
|
+
}
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
---
|
|
288
|
+
|
|
289
|
+
### 4. Scroll-Triggered Animations
|
|
290
|
+
|
|
291
|
+
**Use for:** Elements that appear on scroll (lazy reveal)
|
|
292
|
+
|
|
293
|
+
**Using Intersection Observer:**
|
|
294
|
+
|
|
295
|
+
```tsx
|
|
296
|
+
import { useEffect, useRef } from 'react'
|
|
297
|
+
|
|
298
|
+
export function ScrollReveal({ children }: { children: React.ReactNode }) {
|
|
299
|
+
const ref = useRef<HTMLDivElement>(null)
|
|
300
|
+
|
|
301
|
+
useEffect(() => {
|
|
302
|
+
const observer = new IntersectionObserver(
|
|
303
|
+
([entry]) => {
|
|
304
|
+
if (entry.isIntersecting) {
|
|
305
|
+
entry.target.classList.add('animate-fadeInUp')
|
|
306
|
+
}
|
|
307
|
+
},
|
|
308
|
+
{ threshold: 0.1 }
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
if (ref.current) observer.observe(ref.current)
|
|
312
|
+
return () => observer.disconnect()
|
|
313
|
+
}, [])
|
|
314
|
+
|
|
315
|
+
return (
|
|
316
|
+
<div ref={ref} className="opacity-0">
|
|
317
|
+
{children}
|
|
318
|
+
</div>
|
|
319
|
+
)
|
|
320
|
+
}
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
**CSS:**
|
|
324
|
+
```css
|
|
325
|
+
@keyframes fadeInUp {
|
|
326
|
+
from {
|
|
327
|
+
opacity: 0;
|
|
328
|
+
transform: translateY(30px);
|
|
329
|
+
}
|
|
330
|
+
to {
|
|
331
|
+
opacity: 1;
|
|
332
|
+
transform: translateY(0);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
.animate-fadeInUp {
|
|
337
|
+
animation: fadeInUp 400ms ease-out forwards;
|
|
338
|
+
}
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
---
|
|
342
|
+
|
|
343
|
+
## 🖱️ Micro-interactions
|
|
344
|
+
|
|
345
|
+
### 1. Hover Effects
|
|
346
|
+
|
|
347
|
+
**Card Lift (common pattern):**
|
|
348
|
+
|
|
349
|
+
```css
|
|
350
|
+
.card {
|
|
351
|
+
transition: transform 200ms ease-out,
|
|
352
|
+
box-shadow 200ms ease-out;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
.card:hover {
|
|
356
|
+
transform: translateY(-4px);
|
|
357
|
+
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.15);
|
|
358
|
+
}
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
**Button Scale:**
|
|
362
|
+
|
|
363
|
+
```css
|
|
364
|
+
.button {
|
|
365
|
+
transition: transform 150ms ease-out;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
.button:hover {
|
|
369
|
+
transform: scale(1.05);
|
|
370
|
+
}
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
**Link Underline Animation:**
|
|
374
|
+
|
|
375
|
+
```css
|
|
376
|
+
.link {
|
|
377
|
+
position: relative;
|
|
378
|
+
text-decoration: none;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
.link::after {
|
|
382
|
+
content: '';
|
|
383
|
+
position: absolute;
|
|
384
|
+
bottom: -2px;
|
|
385
|
+
left: 0;
|
|
386
|
+
width: 0;
|
|
387
|
+
height: 2px;
|
|
388
|
+
background: currentColor;
|
|
389
|
+
transition: width 200ms ease-out;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
.link:hover::after {
|
|
393
|
+
width: 100%;
|
|
394
|
+
}
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
---
|
|
398
|
+
|
|
399
|
+
### 2. Click/Press Effects
|
|
400
|
+
|
|
401
|
+
**Scale Down (tactile feedback):**
|
|
402
|
+
|
|
403
|
+
```css
|
|
404
|
+
.button {
|
|
405
|
+
transition: transform 100ms ease-out;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
.button:active {
|
|
409
|
+
transform: scale(0.98);
|
|
410
|
+
}
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
**Ripple Effect (Material Design):**
|
|
414
|
+
|
|
415
|
+
```tsx
|
|
416
|
+
import { useState } from 'react'
|
|
417
|
+
|
|
418
|
+
export function RippleButton({ children, onClick }: any) {
|
|
419
|
+
const [ripples, setRipples] = useState<Array<{ x: number; y: number; id: number }>>([])
|
|
420
|
+
|
|
421
|
+
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
|
|
422
|
+
const rect = e.currentTarget.getBoundingClientRect()
|
|
423
|
+
const x = e.clientX - rect.left
|
|
424
|
+
const y = e.clientY - rect.top
|
|
425
|
+
|
|
426
|
+
setRipples([...ripples, { x, y, id: Date.now() }])
|
|
427
|
+
setTimeout(() => setRipples((prev) => prev.slice(1)), 600)
|
|
428
|
+
|
|
429
|
+
onClick?.(e)
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
return (
|
|
433
|
+
<button className="relative overflow-hidden" onClick={handleClick}>
|
|
434
|
+
{children}
|
|
435
|
+
{ripples.map((ripple) => (
|
|
436
|
+
<span
|
|
437
|
+
key={ripple.id}
|
|
438
|
+
className="absolute bg-white/30 rounded-full animate-ripple"
|
|
439
|
+
style={{
|
|
440
|
+
left: ripple.x,
|
|
441
|
+
top: ripple.y,
|
|
442
|
+
width: 10,
|
|
443
|
+
height: 10,
|
|
444
|
+
}}
|
|
445
|
+
/>
|
|
446
|
+
))}
|
|
447
|
+
</button>
|
|
448
|
+
)
|
|
449
|
+
}
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
**CSS:**
|
|
453
|
+
```css
|
|
454
|
+
@keyframes ripple {
|
|
455
|
+
to {
|
|
456
|
+
transform: translate(-50%, -50%) scale(10);
|
|
457
|
+
opacity: 0;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
.animate-ripple {
|
|
462
|
+
animation: ripple 600ms ease-out forwards;
|
|
463
|
+
transform: translate(-50%, -50%);
|
|
464
|
+
}
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
---
|
|
468
|
+
|
|
469
|
+
### 3. Focus States
|
|
470
|
+
|
|
471
|
+
**Ring Animation:**
|
|
472
|
+
|
|
473
|
+
```css
|
|
474
|
+
.input:focus {
|
|
475
|
+
outline: none;
|
|
476
|
+
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.5);
|
|
477
|
+
transition: box-shadow 150ms ease-out;
|
|
478
|
+
}
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
**Border Glow:**
|
|
482
|
+
|
|
483
|
+
```css
|
|
484
|
+
.input {
|
|
485
|
+
border: 2px solid transparent;
|
|
486
|
+
transition: border-color 200ms ease-out;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
.input:focus {
|
|
490
|
+
border-color: var(--color-primary);
|
|
491
|
+
}
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
---
|
|
495
|
+
|
|
496
|
+
## 🔄 Loading States
|
|
497
|
+
|
|
498
|
+
### 1. Skeleton Loader
|
|
499
|
+
|
|
500
|
+
```tsx
|
|
501
|
+
export function SkeletonCard() {
|
|
502
|
+
return (
|
|
503
|
+
<div className="border rounded-lg p-6 space-y-4">
|
|
504
|
+
<div className="h-4 bg-gray-200 rounded animate-pulse" />
|
|
505
|
+
<div className="h-4 bg-gray-200 rounded animate-pulse w-3/4" />
|
|
506
|
+
<div className="h-8 bg-gray-200 rounded animate-pulse" />
|
|
507
|
+
</div>
|
|
508
|
+
)
|
|
509
|
+
}
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
**CSS:**
|
|
513
|
+
```css
|
|
514
|
+
@keyframes pulse {
|
|
515
|
+
0%, 100% {
|
|
516
|
+
opacity: 1;
|
|
517
|
+
}
|
|
518
|
+
50% {
|
|
519
|
+
opacity: 0.5;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
.animate-pulse {
|
|
524
|
+
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
|
525
|
+
}
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
---
|
|
529
|
+
|
|
530
|
+
### 2. Shimmer Effect
|
|
531
|
+
|
|
532
|
+
```css
|
|
533
|
+
@keyframes shimmer {
|
|
534
|
+
0% {
|
|
535
|
+
background-position: -1000px 0;
|
|
536
|
+
}
|
|
537
|
+
100% {
|
|
538
|
+
background-position: 1000px 0;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
.skeleton {
|
|
543
|
+
background: linear-gradient(
|
|
544
|
+
90deg,
|
|
545
|
+
#f0f0f0 25%,
|
|
546
|
+
#e0e0e0 50%,
|
|
547
|
+
#f0f0f0 75%
|
|
548
|
+
);
|
|
549
|
+
background-size: 1000px 100%;
|
|
550
|
+
animation: shimmer 2s infinite;
|
|
551
|
+
}
|
|
552
|
+
```
|
|
553
|
+
|
|
554
|
+
---
|
|
555
|
+
|
|
556
|
+
### 3. Spinner (Button Loading)
|
|
557
|
+
|
|
558
|
+
```tsx
|
|
559
|
+
export function ButtonWithSpinner({ loading, children, ...props }: any) {
|
|
560
|
+
return (
|
|
561
|
+
<button {...props} disabled={loading}>
|
|
562
|
+
{loading ? (
|
|
563
|
+
<div className="flex items-center gap-2">
|
|
564
|
+
<div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin" />
|
|
565
|
+
Loading...
|
|
566
|
+
</div>
|
|
567
|
+
) : (
|
|
568
|
+
children
|
|
569
|
+
)}
|
|
570
|
+
</button>
|
|
571
|
+
)
|
|
572
|
+
}
|
|
573
|
+
```
|
|
574
|
+
|
|
575
|
+
**CSS:**
|
|
576
|
+
```css
|
|
577
|
+
@keyframes spin {
|
|
578
|
+
to {
|
|
579
|
+
transform: rotate(360deg);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
.animate-spin {
|
|
584
|
+
animation: spin 1s linear infinite;
|
|
585
|
+
}
|
|
586
|
+
```
|
|
587
|
+
|
|
588
|
+
---
|
|
589
|
+
|
|
590
|
+
## 🎭 Transition Choreography
|
|
591
|
+
|
|
592
|
+
### 1. Stagger Effect (List Items)
|
|
593
|
+
|
|
594
|
+
**Animate list items with delay:**
|
|
595
|
+
|
|
596
|
+
```tsx
|
|
597
|
+
export function StaggerList({ items }: { items: string[] }) {
|
|
598
|
+
return (
|
|
599
|
+
<ul>
|
|
600
|
+
{items.map((item, index) => (
|
|
601
|
+
<li
|
|
602
|
+
key={item}
|
|
603
|
+
className="animate-fadeInUp"
|
|
604
|
+
style={{ animationDelay: `${index * 100}ms` }}
|
|
605
|
+
>
|
|
606
|
+
{item}
|
|
607
|
+
</li>
|
|
608
|
+
))}
|
|
609
|
+
</ul>
|
|
610
|
+
)
|
|
611
|
+
}
|
|
612
|
+
```
|
|
613
|
+
|
|
614
|
+
---
|
|
615
|
+
|
|
616
|
+
### 2. Page Transitions
|
|
617
|
+
|
|
618
|
+
```tsx
|
|
619
|
+
import { AnimatePresence, motion } from 'framer-motion'
|
|
620
|
+
|
|
621
|
+
export function PageTransition({ children }: { children: React.ReactNode }) {
|
|
622
|
+
return (
|
|
623
|
+
<AnimatePresence mode="wait">
|
|
624
|
+
<motion.div
|
|
625
|
+
initial={{ opacity: 0, y: 20 }}
|
|
626
|
+
animate={{ opacity: 1, y: 0 }}
|
|
627
|
+
exit={{ opacity: 0, y: -20 }}
|
|
628
|
+
transition={{ duration: 0.3 }}
|
|
629
|
+
>
|
|
630
|
+
{children}
|
|
631
|
+
</motion.div>
|
|
632
|
+
</AnimatePresence>
|
|
633
|
+
)
|
|
634
|
+
}
|
|
635
|
+
```
|
|
636
|
+
|
|
637
|
+
---
|
|
638
|
+
|
|
639
|
+
## ♿ Accessibility: Reduced Motion
|
|
640
|
+
|
|
641
|
+
**Always respect user preferences:**
|
|
642
|
+
|
|
643
|
+
```css
|
|
644
|
+
/* Default: animations enabled */
|
|
645
|
+
.element {
|
|
646
|
+
animation: slideIn 300ms ease-out;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
/* User prefers reduced motion */
|
|
650
|
+
@media (prefers-reduced-motion: reduce) {
|
|
651
|
+
.element {
|
|
652
|
+
animation: none;
|
|
653
|
+
transition: none;
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
```
|
|
657
|
+
|
|
658
|
+
**React Hook:**
|
|
659
|
+
|
|
660
|
+
```tsx
|
|
661
|
+
import { useEffect, useState } from 'react'
|
|
662
|
+
|
|
663
|
+
export function usePrefersReducedMotion() {
|
|
664
|
+
const [prefersReducedMotion, setPrefersReducedMotion] = useState(false)
|
|
665
|
+
|
|
666
|
+
useEffect(() => {
|
|
667
|
+
const mediaQuery = window.matchMedia('(prefers-reduced-motion: reduce)')
|
|
668
|
+
setPrefersReducedMotion(mediaQuery.matches)
|
|
669
|
+
|
|
670
|
+
const handler = (e: MediaQueryListEvent) => setPrefersReducedMotion(e.matches)
|
|
671
|
+
mediaQuery.addEventListener('change', handler)
|
|
672
|
+
return () => mediaQuery.removeEventListener('change', handler)
|
|
673
|
+
}, [])
|
|
674
|
+
|
|
675
|
+
return prefersReducedMotion
|
|
676
|
+
}
|
|
677
|
+
```
|
|
678
|
+
|
|
679
|
+
---
|
|
680
|
+
|
|
681
|
+
## 📋 Animation Checklist
|
|
682
|
+
|
|
683
|
+
**Before implementing animations:**
|
|
684
|
+
|
|
685
|
+
- [ ] **Performance:** Using `transform`/`opacity` only? ✓
|
|
686
|
+
- [ ] **Duration:** 150-600ms (appropriate for use case)? ✓
|
|
687
|
+
- [ ] **Easing:** Using `ease-out` (most cases) or `ease-in-out`? ✓
|
|
688
|
+
- [ ] **Purpose:** Animation has clear purpose (not decorative)? ✓
|
|
689
|
+
- [ ] **Accessibility:** Respects `prefers-reduced-motion`? ✓
|
|
690
|
+
- [ ] **Subtlety:** Animation enhances, doesn't distract? ✓
|
|
691
|
+
|
|
692
|
+
---
|
|
693
|
+
|
|
694
|
+
## 🚨 Common Mistakes
|
|
695
|
+
|
|
696
|
+
### ❌ Mistake 1: Animating layout properties
|
|
697
|
+
|
|
698
|
+
```css
|
|
699
|
+
/* ❌ BAD: Width triggers layout recalculation */
|
|
700
|
+
.element:hover {
|
|
701
|
+
width: 200px;
|
|
702
|
+
transition: width 300ms;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
/* ✅ GOOD: Scale uses transform (GPU) */
|
|
706
|
+
.element:hover {
|
|
707
|
+
transform: scaleX(1.2);
|
|
708
|
+
transition: transform 300ms;
|
|
709
|
+
}
|
|
710
|
+
```
|
|
711
|
+
|
|
712
|
+
---
|
|
713
|
+
|
|
714
|
+
### ❌ Mistake 2: Too long animations
|
|
715
|
+
|
|
716
|
+
```css
|
|
717
|
+
/* ❌ BAD: Feels sluggish */
|
|
718
|
+
.button:hover {
|
|
719
|
+
transform: scale(1.05);
|
|
720
|
+
transition: transform 1000ms;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
/* ✅ GOOD: Snappy and responsive */
|
|
724
|
+
.button:hover {
|
|
725
|
+
transform: scale(1.05);
|
|
726
|
+
transition: transform 150ms ease-out;
|
|
727
|
+
}
|
|
728
|
+
```
|
|
729
|
+
|
|
730
|
+
---
|
|
731
|
+
|
|
732
|
+
### ❌ Mistake 3: Forgetting reduced motion
|
|
733
|
+
|
|
734
|
+
```css
|
|
735
|
+
/* ❌ BAD: Ignores user preference */
|
|
736
|
+
.element {
|
|
737
|
+
animation: spinForever 2s infinite;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
/* ✅ GOOD: Respects accessibility */
|
|
741
|
+
.element {
|
|
742
|
+
animation: spinForever 2s infinite;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
@media (prefers-reduced-motion: reduce) {
|
|
746
|
+
.element {
|
|
747
|
+
animation: none;
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
```
|
|
751
|
+
|
|
752
|
+
---
|
|
753
|
+
|
|
754
|
+
## 📚 Additional Resources
|
|
755
|
+
|
|
756
|
+
**Libraries:**
|
|
757
|
+
- **Framer Motion:** https://www.framer.com/motion/ (React animations)
|
|
758
|
+
- **GSAP:** https://greensock.com/gsap/ (High-performance animations)
|
|
759
|
+
- **Auto Animate:** https://auto-animate.formkit.com/ (Zero-config animations)
|
|
760
|
+
|
|
761
|
+
**Inspiration:**
|
|
762
|
+
- **UI Movement:** https://uimovement.com/
|
|
763
|
+
- **Lottie Files:** https://lottiefiles.com/
|
|
764
|
+
- **Codrops:** https://tympanus.net/codrops/
|
|
765
|
+
|
|
766
|
+
---
|
|
767
|
+
|
|
768
|
+
**💡 Remember:** The best animations are the ones users don't consciously notice - they just make the UI feel better!
|