0
# Animation & Transitions
1
2
Enter/exit animation management with CSS integration and transition coordination for React components.
3
4
## Capabilities
5
6
### Enter Animations
7
8
Hook for managing enter animations when components mount or become visible.
9
10
```typescript { .api }
11
/**
12
* Manages enter animations for components
13
* @param ref - RefObject to animated element
14
* @param isReady - Whether animation should start (default: true)
15
* @returns Boolean indicating if element is in entering state
16
*/
17
function useEnterAnimation(ref: RefObject<HTMLElement>, isReady?: boolean): boolean;
18
```
19
20
**Usage Examples:**
21
22
```typescript
23
import { useEnterAnimation } from "@react-aria/utils";
24
25
function FadeInComponent({ children, show = true }) {
26
const elementRef = useRef<HTMLDivElement>(null);
27
const isEntering = useEnterAnimation(elementRef, show);
28
29
return (
30
<div
31
ref={elementRef}
32
className={`fade-component ${isEntering ? 'entering' : 'entered'}`}
33
style={{
34
opacity: isEntering ? 0 : 1,
35
transition: 'opacity 300ms ease-in-out'
36
}}
37
>
38
{children}
39
</div>
40
);
41
}
42
43
// CSS-based keyframe animation
44
function SlideInComponent({ children, show = true }) {
45
const elementRef = useRef<HTMLDivElement>(null);
46
const isEntering = useEnterAnimation(elementRef, show);
47
48
return (
49
<div
50
ref={elementRef}
51
className={`slide-component ${isEntering ? 'slide-enter' : 'slide-entered'}`}
52
>
53
{children}
54
</div>
55
);
56
}
57
58
// CSS for slideInComponent
59
/*
60
.slide-component {
61
transform: translateX(-100%);
62
transition: transform 400ms cubic-bezier(0.4, 0, 0.2, 1);
63
}
64
65
.slide-component.slide-entered {
66
transform: translateX(0);
67
}
68
69
.slide-component.slide-enter {
70
animation: slideIn 400ms cubic-bezier(0.4, 0, 0.2, 1) forwards;
71
}
72
73
@keyframes slideIn {
74
from { transform: translateX(-100%); }
75
to { transform: translateX(0); }
76
}
77
*/
78
```
79
80
### Exit Animations
81
82
Hook for managing exit animations before components unmount or become hidden.
83
84
```typescript { .api }
85
/**
86
* Manages exit animations before unmounting
87
* @param ref - RefObject to animated element
88
* @param isOpen - Whether component should be open/visible
89
* @returns Boolean indicating if element is in exiting state
90
*/
91
function useExitAnimation(ref: RefObject<HTMLElement>, isOpen: boolean): boolean;
92
```
93
94
**Usage Examples:**
95
96
```typescript
97
import { useExitAnimation } from "@react-aria/utils";
98
99
function Modal({ isOpen, onClose, children }) {
100
const modalRef = useRef<HTMLDivElement>(null);
101
const isExiting = useExitAnimation(modalRef, isOpen);
102
103
// Don't render if closed and not exiting
104
if (!isOpen && !isExiting) return null;
105
106
return (
107
<div
108
className={`modal-backdrop ${isExiting ? 'exiting' : ''}`}
109
style={{
110
opacity: isExiting ? 0 : 1,
111
transition: 'opacity 250ms ease-out'
112
}}
113
>
114
<div
115
ref={modalRef}
116
className={`modal-content ${isExiting ? 'modal-exit' : 'modal-enter'}`}
117
style={{
118
transform: isExiting ? 'scale(0.95) translateY(-10px)' : 'scale(1) translateY(0)',
119
transition: 'transform 250ms ease-out'
120
}}
121
>
122
<button onClick={onClose}>Close</button>
123
{children}
124
</div>
125
</div>
126
);
127
}
128
129
// Notification with auto-dismiss animation
130
function Notification({ message, onDismiss, autoClose = 5000 }) {
131
const [isOpen, setIsOpen] = useState(true);
132
const notificationRef = useRef<HTMLDivElement>(null);
133
const isExiting = useExitAnimation(notificationRef, isOpen);
134
135
useEffect(() => {
136
const timer = setTimeout(() => setIsOpen(false), autoClose);
137
return () => clearTimeout(timer);
138
}, [autoClose]);
139
140
useEffect(() => {
141
if (!isOpen && !isExiting) {
142
onDismiss();
143
}
144
}, [isOpen, isExiting, onDismiss]);
145
146
if (!isOpen && !isExiting) return null;
147
148
return (
149
<div
150
ref={notificationRef}
151
className={`notification ${isExiting ? 'notification-exit' : 'notification-enter'}`}
152
style={{
153
transform: isExiting ? 'translateX(100%)' : 'translateX(0)',
154
opacity: isExiting ? 0 : 1,
155
transition: 'transform 300ms ease-in-out, opacity 300ms ease-in-out'
156
}}
157
>
158
{message}
159
<button onClick={() => setIsOpen(false)}>×</button>
160
</div>
161
);
162
}
163
```
164
165
### Transition Coordination
166
167
Function for executing callbacks after all CSS transitions complete.
168
169
```typescript { .api }
170
/**
171
* Executes callback after all CSS transitions complete
172
* @param fn - Function to execute after transitions
173
*/
174
function runAfterTransition(fn: () => void): void;
175
```
176
177
**Usage Examples:**
178
179
```typescript
180
import { runAfterTransition } from "@react-aria/utils";
181
182
function AnimatedList({ items, onAnimationComplete }) {
183
const [isAnimating, setIsAnimating] = useState(false);
184
185
const animateList = () => {
186
setIsAnimating(true);
187
188
// Start animation by adding CSS class
189
const listElement = document.querySelector('.animated-list');
190
listElement?.classList.add('animate');
191
192
// Wait for all transitions to complete
193
runAfterTransition(() => {
194
setIsAnimating(false);
195
listElement?.classList.remove('animate');
196
onAnimationComplete();
197
});
198
};
199
200
return (
201
<div>
202
<button onClick={animateList} disabled={isAnimating}>
203
{isAnimating ? 'Animating...' : 'Animate List'}
204
</button>
205
<ul className="animated-list">
206
{items.map(item => (
207
<li key={item.id}>{item.name}</li>
208
))}
209
</ul>
210
</div>
211
);
212
}
213
214
// Complex animation sequence
215
function SequentialAnimation({ steps, onComplete }) {
216
const [currentStep, setCurrentStep] = useState(0);
217
const elementRefs = useRef<(HTMLDivElement | null)[]>([]);
218
219
const animateStep = (stepIndex: number) => {
220
const element = elementRefs.current[stepIndex];
221
if (!element) return;
222
223
// Add animation class
224
element.classList.add('step-animate');
225
226
// Wait for transition to complete
227
runAfterTransition(() => {
228
element.classList.remove('step-animate');
229
230
if (stepIndex < steps.length - 1) {
231
setCurrentStep(stepIndex + 1);
232
// Animate next step
233
setTimeout(() => animateStep(stepIndex + 1), 100);
234
} else {
235
onComplete();
236
}
237
});
238
};
239
240
useEffect(() => {
241
if (currentStep < steps.length) {
242
animateStep(currentStep);
243
}
244
}, [currentStep, steps.length]);
245
246
return (
247
<div>
248
{steps.map((step, index) => (
249
<div
250
key={index}
251
ref={el => elementRefs.current[index] = el}
252
className={`step ${index <= currentStep ? 'active' : ''}`}
253
>
254
{step.content}
255
</div>
256
))}
257
</div>
258
);
259
}
260
```
261
262
### Advanced Animation Patterns
263
264
Complex animation scenarios combining enter/exit animations with coordination:
265
266
```typescript
267
import { useEnterAnimation, useExitAnimation, runAfterTransition } from "@react-aria/utils";
268
269
function StaggeredList({ items, isVisible }) {
270
const listRef = useRef<HTMLUListElement>(null);
271
const isEntering = useEnterAnimation(listRef, isVisible);
272
const isExiting = useExitAnimation(listRef, isVisible);
273
274
useEffect(() => {
275
if (!listRef.current) return;
276
277
const listItems = Array.from(listRef.current.children) as HTMLElement[];
278
279
if (isEntering) {
280
// Stagger enter animations
281
listItems.forEach((item, index) => {
282
item.style.transitionDelay = `${index * 50}ms`;
283
item.classList.add('item-enter');
284
});
285
} else if (isExiting) {
286
// Reverse stagger for exit
287
listItems.forEach((item, index) => {
288
item.style.transitionDelay = `${(listItems.length - index - 1) * 30}ms`;
289
item.classList.add('item-exit');
290
});
291
}
292
}, [isEntering, isExiting]);
293
294
if (!isVisible && !isExiting) return null;
295
296
return (
297
<ul ref={listRef} className="staggered-list">
298
{items.map(item => (
299
<li key={item.id}>{item.name}</li>
300
))}
301
</ul>
302
);
303
}
304
305
// Shared element transition
306
function SharedElementTransition({ fromElement, toElement, onTransitionEnd }) {
307
useEffect(() => {
308
if (!fromElement || !toElement) return;
309
310
// Get positions
311
const fromRect = fromElement.getBoundingClientRect();
312
const toRect = toElement.getBoundingClientRect();
313
314
// Create shared element
315
const sharedElement = fromElement.cloneNode(true) as HTMLElement;
316
sharedElement.style.position = 'fixed';
317
sharedElement.style.top = `${fromRect.top}px`;
318
sharedElement.style.left = `${fromRect.left}px`;
319
sharedElement.style.width = `${fromRect.width}px`;
320
sharedElement.style.height = `${fromRect.height}px`;
321
sharedElement.style.zIndex = '1000';
322
sharedElement.style.pointerEvents = 'none';
323
324
document.body.appendChild(sharedElement);
325
326
// Hide original elements
327
fromElement.style.opacity = '0';
328
toElement.style.opacity = '0';
329
330
// Animate to target position
331
requestAnimationFrame(() => {
332
sharedElement.style.transition = 'all 400ms cubic-bezier(0.4, 0, 0.2, 1)';
333
sharedElement.style.top = `${toRect.top}px`;
334
sharedElement.style.left = `${toRect.left}px`;
335
sharedElement.style.width = `${toRect.width}px`;
336
sharedElement.style.height = `${toRect.height}px`;
337
});
338
339
runAfterTransition(() => {
340
// Clean up
341
document.body.removeChild(sharedElement);
342
fromElement.style.opacity = '';
343
toElement.style.opacity = '';
344
onTransitionEnd();
345
});
346
}, [fromElement, toElement, onTransitionEnd]);
347
348
return null;
349
}
350
351
// Page transition component
352
function PageTransition({ currentPage, nextPage, direction = 'forward' }) {
353
const containerRef = useRef<HTMLDivElement>(null);
354
const [isTransitioning, setIsTransitioning] = useState(false);
355
356
const transitionToPage = (newPage: React.ReactNode) => {
357
if (!containerRef.current || isTransitioning) return;
358
359
setIsTransitioning(true);
360
const container = containerRef.current;
361
362
// Add transition classes
363
container.classList.add('page-transition');
364
container.classList.add(direction === 'forward' ? 'forward' : 'backward');
365
366
runAfterTransition(() => {
367
// Update page content
368
setCurrentPage(newPage);
369
370
// Remove transition classes
371
container.classList.remove('page-transition', 'forward', 'backward');
372
setIsTransitioning(false);
373
});
374
};
375
376
return (
377
<div ref={containerRef} className="page-container">
378
{currentPage}
379
</div>
380
);
381
}
382
```
383
384
## CSS Integration Examples
385
386
CSS classes that work well with these animation hooks:
387
388
```css
389
/* Enter animation classes */
390
.fade-component {
391
transition: opacity 300ms ease-in-out;
392
}
393
394
.fade-component.entering {
395
opacity: 0;
396
}
397
398
.fade-component.entered {
399
opacity: 1;
400
}
401
402
/* Exit animation classes */
403
.modal-backdrop {
404
transition: opacity 250ms ease-out;
405
}
406
407
.modal-backdrop.exiting {
408
opacity: 0;
409
}
410
411
.modal-content {
412
transition: transform 250ms ease-out;
413
}
414
415
.modal-content.modal-exit {
416
transform: scale(0.95) translateY(-10px);
417
}
418
419
/* Staggered list animations */
420
.staggered-list li {
421
opacity: 0;
422
transform: translateY(20px);
423
transition: opacity 300ms ease-out, transform 300ms ease-out;
424
}
425
426
.staggered-list li.item-enter {
427
opacity: 1;
428
transform: translateY(0);
429
}
430
431
.staggered-list li.item-exit {
432
opacity: 0;
433
transform: translateY(-10px);
434
}
435
436
/* Page transitions */
437
.page-container.page-transition.forward {
438
transform: translateX(-100%);
439
}
440
441
.page-container.page-transition.backward {
442
transform: translateX(100%);
443
}
444
```