or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

index.mdmotion.mdspring-system.mdstaggered-motion.mdtransition-motion.md

staggered-motion.mddocs/

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

```