or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

animation-and-transitions.mdevent-management.mdfocus-and-accessibility.mdid-and-refs.mdindex.mdlinks-and-navigation.mdmiscellaneous-utilities.mdplatform-detection.mdprops-and-events.mdscrolling-and-layout.mdshadow-dom-support.mdstate-and-effects.mdvirtual-events-and-input.md

animation-and-transitions.mddocs/

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

```