0
# StaggeredMotion Component
1
2
The StaggeredMotion component creates cascading animations where each element's motion depends on the state of previous elements. This creates natural-looking staggered effects, perfect for list transitions, domino effects, and sequential animations.
3
4
## Capabilities
5
6
### StaggeredMotion Component
7
8
Creates multiple animated elements where each element's target style can depend on the previous elements' current interpolated styles.
9
10
```javascript { .api }
11
/**
12
* Multiple element animation where each element depends on previous ones
13
* Perfect for cascading effects and staggered list transitions
14
*/
15
class StaggeredMotion extends React.Component {
16
static propTypes: {
17
/** Initial style values for all elements (optional) */
18
defaultStyles?: Array<PlainStyle>,
19
/** Function returning target styles based on previous element states (required) */
20
styles: (previousInterpolatedStyles: ?Array<PlainStyle>) => Array<Style>,
21
/** Render function receiving array of interpolated styles (required) */
22
children: (interpolatedStyles: Array<PlainStyle>) => ReactElement
23
}
24
}
25
```
26
27
**Usage Examples:**
28
29
```javascript
30
import React, { useState } from 'react';
31
import { StaggeredMotion, spring } from 'react-motion';
32
33
// Basic staggered animation
34
function StaggeredList() {
35
const [mouseX, setMouseX] = useState(0);
36
37
return (
38
<StaggeredMotion
39
defaultStyles={[{x: 0}, {x: 0}, {x: 0}, {x: 0}]}
40
styles={prevInterpolatedStyles => prevInterpolatedStyles.map((_, i) => {
41
return i === 0
42
? {x: spring(mouseX)}
43
: {x: spring(prevInterpolatedStyles[i - 1].x)};
44
})}
45
>
46
{interpolatedStyles => (
47
<div
48
onMouseMove={e => setMouseX(e.clientX)}
49
style={{height: '400px', background: '#f0f0f0'}}
50
>
51
{interpolatedStyles.map((style, i) => (
52
<div
53
key={i}
54
style={{
55
position: 'absolute',
56
transform: `translateX(${style.x}px) translateY(${i * 50}px)`,
57
width: '40px',
58
height: '40px',
59
background: `hsl(${i * 60}, 70%, 50%)`,
60
borderRadius: '20px'
61
}}
62
/>
63
))}
64
</div>
65
)}
66
</StaggeredMotion>
67
);
68
}
69
70
// Staggered list items
71
function StaggeredListItems() {
72
const [items, setItems] = useState(['Item 1', 'Item 2', 'Item 3']);
73
const [expanded, setExpanded] = useState(false);
74
75
return (
76
<div>
77
<button onClick={() => setExpanded(!expanded)}>
78
Toggle List
79
</button>
80
81
<StaggeredMotion
82
defaultStyles={items.map(() => ({opacity: 0, y: -20}))}
83
styles={prevInterpolatedStyles =>
84
prevInterpolatedStyles.map((_, i) => {
85
const prevStyle = i === 0 ? null : prevInterpolatedStyles[i - 1];
86
const baseDelay = expanded ? 0 : 1;
87
const followDelay = prevStyle ? prevStyle.opacity * 0.8 : baseDelay;
88
89
return {
90
opacity: spring(expanded ? 1 : 0),
91
y: spring(expanded ? 0 : -20, {
92
stiffness: 120,
93
damping: 17
94
})
95
};
96
})
97
}
98
>
99
{interpolatedStyles => (
100
<div>
101
{interpolatedStyles.map((style, i) => (
102
<div
103
key={i}
104
style={{
105
opacity: style.opacity,
106
transform: `translateY(${style.y}px)`,
107
padding: '10px',
108
margin: '5px 0',
109
background: 'white',
110
borderRadius: '4px',
111
boxShadow: '0 2px 4px rgba(0,0,0,0.1)'
112
}}
113
>
114
{items[i]}
115
</div>
116
))}
117
</div>
118
)}
119
</StaggeredMotion>
120
</div>
121
);
122
}
123
124
// Pendulum chain effect
125
function PendulumChain() {
126
const [angle, setAngle] = useState(0);
127
128
return (
129
<StaggeredMotion
130
defaultStyles={new Array(5).fill({rotate: 0})}
131
styles={prevInterpolatedStyles => prevInterpolatedStyles.map((_, i) => {
132
return i === 0
133
? {rotate: spring(angle)}
134
: {rotate: spring(prevInterpolatedStyles[i - 1].rotate * 0.8)};
135
})}
136
>
137
{interpolatedStyles => (
138
<div style={{padding: '50px'}}>
139
<button onClick={() => setAngle(angle === 0 ? 45 : 0)}>
140
Swing Pendulum
141
</button>
142
143
<div style={{position: 'relative', height: '300px'}}>
144
{interpolatedStyles.map((style, i) => (
145
<div
146
key={i}
147
style={{
148
position: 'absolute',
149
left: `${50 + i * 40}px`,
150
top: '50px',
151
width: '2px',
152
height: '150px',
153
background: '#333',
154
transformOrigin: 'top center',
155
transform: `rotate(${style.rotate}deg)`
156
}}
157
>
158
<div
159
style={{
160
position: 'absolute',
161
bottom: '-10px',
162
left: '-8px',
163
width: '16px',
164
height: '16px',
165
background: '#e74c3c',
166
borderRadius: '50%'
167
}}
168
/>
169
</div>
170
))}
171
</div>
172
</div>
173
)}
174
</StaggeredMotion>
175
);
176
}
177
```
178
179
### defaultStyles Property
180
181
Optional array of initial style values for all animated elements. If omitted, initial values are extracted from the styles function.
182
183
```javascript { .api }
184
/**
185
* Initial style values for all elements
186
* Array length determines number of animated elements
187
*/
188
defaultStyles?: Array<PlainStyle>;
189
```
190
191
### styles Property
192
193
Function that receives the previous frame's interpolated styles and returns an array of target styles. This is where the staggering logic is implemented.
194
195
```javascript { .api }
196
/**
197
* Function returning target styles based on previous element states
198
* Called every frame with current interpolated values
199
* @param previousInterpolatedStyles - Current values from previous frame
200
* @returns Array of target Style objects
201
*/
202
styles: (previousInterpolatedStyles: ?Array<PlainStyle>) => Array<Style>;
203
```
204
205
### children Property
206
207
Render function that receives the current interpolated styles for all elements and returns a React element.
208
209
```javascript { .api }
210
/**
211
* Render function receiving array of interpolated styles
212
* Called on every animation frame with current values for all elements
213
*/
214
children: (interpolatedStyles: Array<PlainStyle>) => ReactElement;
215
```
216
217
## Animation Behavior
218
219
### Cascading Logic
220
221
The key to StaggeredMotion is in the styles function:
222
223
```javascript
224
styles={prevInterpolatedStyles => prevInterpolatedStyles.map((_, i) => {
225
if (i === 0) {
226
// First element follows external state
227
return {x: spring(targetValue)};
228
} else {
229
// Subsequent elements follow previous element
230
return {x: spring(prevInterpolatedStyles[i - 1].x)};
231
}
232
})}
233
```
234
235
### Staggering Patterns
236
237
**Linear Following**: Each element directly follows the previous
238
```javascript
239
styles={prev => prev.map((_, i) =>
240
i === 0
241
? {x: spring(leader)}
242
: {x: spring(prev[i - 1].x)}
243
)}
244
```
245
246
**Delayed Following**: Add delay or damping to the chain
247
```javascript
248
styles={prev => prev.map((_, i) =>
249
i === 0
250
? {x: spring(leader)}
251
: {x: spring(prev[i - 1].x * 0.8)} // 80% of previous
252
)}
253
```
254
255
**Wave Effects**: Use mathematical functions for wave-like motion
256
```javascript
257
styles={prev => prev.map((_, i) => ({
258
y: spring(Math.sin(time + i * 0.5) * amplitude)
259
}))}
260
```
261
262
### Performance Considerations
263
264
- Each element depends on previous calculations, so changes cascade through the chain
265
- Longer chains may have slight performance impact
266
- Animation stops when all elements reach their targets
267
268
## Common Patterns
269
270
### Mouse Following Chain
271
272
```javascript
273
function MouseChain() {
274
const [mouse, setMouse] = useState({x: 0, y: 0});
275
276
return (
277
<StaggeredMotion
278
defaultStyles={new Array(10).fill({x: 0, y: 0})}
279
styles={prev => prev.map((_, i) =>
280
i === 0
281
? {x: spring(mouse.x), y: spring(mouse.y)}
282
: {
283
x: spring(prev[i - 1].x, {stiffness: 300, damping: 30}),
284
y: spring(prev[i - 1].y, {stiffness: 300, damping: 30})
285
}
286
)}
287
>
288
{styles => (
289
<div
290
onMouseMove={e => setMouse({x: e.clientX, y: e.clientY})}
291
style={{height: '100vh', background: '#000'}}
292
>
293
{styles.map((style, i) => (
294
<div
295
key={i}
296
style={{
297
position: 'absolute',
298
left: style.x,
299
top: style.y,
300
width: 20 - i,
301
height: 20 - i,
302
background: `hsl(${i * 30}, 70%, 50%)`,
303
borderRadius: '50%',
304
transform: 'translate(-50%, -50%)'
305
}}
306
/>
307
))}
308
</div>
309
)}
310
</StaggeredMotion>
311
);
312
}
313
```
314
315
### Accordion Effect
316
317
```javascript
318
function StaggeredAccordion() {
319
const [openIndex, setOpenIndex] = useState(null);
320
const items = ['Section 1', 'Section 2', 'Section 3', 'Section 4'];
321
322
return (
323
<StaggeredMotion
324
defaultStyles={items.map(() => ({height: 40, opacity: 1}))}
325
styles={prev => prev.map((_, i) => {
326
const isOpen = openIndex === i;
327
const prevOpen = i > 0 && prev[i - 1].height > 40;
328
329
return {
330
height: spring(isOpen ? 200 : 40),
331
opacity: spring(isOpen || !prevOpen ? 1 : 0.6)
332
};
333
})}
334
>
335
{styles => (
336
<div>
337
{styles.map((style, i) => (
338
<div
339
key={i}
340
style={{
341
height: style.height,
342
opacity: style.opacity,
343
background: '#f8f9fa',
344
border: '1px solid #dee2e6',
345
margin: '2px 0',
346
overflow: 'hidden',
347
cursor: 'pointer'
348
}}
349
onClick={() => setOpenIndex(openIndex === i ? null : i)}
350
>
351
<div style={{padding: '10px', fontWeight: 'bold'}}>
352
{items[i]}
353
</div>
354
{style.height > 40 && (
355
<div style={{padding: '0 10px 10px'}}>
356
Content for {items[i]}...
357
</div>
358
)}
359
</div>
360
))}
361
</div>
362
)}
363
</StaggeredMotion>
364
);
365
}
366
```
367
368
### Loading Dots
369
370
```javascript
371
function LoadingDots() {
372
const [time, setTime] = useState(0);
373
374
React.useEffect(() => {
375
const interval = setInterval(() => {
376
setTime(t => t + 0.1);
377
}, 16);
378
return () => clearInterval(interval);
379
}, []);
380
381
return (
382
<StaggeredMotion
383
defaultStyles={[{y: 0}, {y: 0}, {y: 0}]}
384
styles={prev => prev.map((_, i) => ({
385
y: spring(Math.sin(time + i * 0.8) * 10)
386
}))}
387
>
388
{styles => (
389
<div style={{display: 'flex', gap: '8px', padding: '20px'}}>
390
{styles.map((style, i) => (
391
<div
392
key={i}
393
style={{
394
width: '12px',
395
height: '12px',
396
background: '#007bff',
397
borderRadius: '50%',
398
transform: `translateY(${style.y}px)`
399
}}
400
/>
401
))}
402
</div>
403
)}
404
</StaggeredMotion>
405
);
406
}
407
```