When building websites and apps with designers, we want things to flow smoothly. This often involves making things appear with a staggering effect, i.e. one at a time.
Doing this in React can be tricky. React encourages component isolation which can make it difficult to coordinate animation across components, especially when they are deeply nested.
React Stagger provides a low-level Transition-like Stagger
component that
calculates a rendering delay based on other Stagger instances.
This module is distributed via npm which is bundled with node and
should be installed as one of your project's dependencies
:
npm install --save react-stagger
import React from 'react'
import {render} from 'react-dom'
import Stagger from 'react-stagger'
render(
<>
<Stagger>{({delay}) => <p>{delay}ms</p>}</Stagger>
<Stagger>{({delay}) => <p>{delay}ms</p>}</Stagger>
<Stagger delay={200}>{({delay}) => <p>{delay}ms</p>}</Stagger>
</>,
document.getElementById('root'),
)
// Renders:
// 0ms
// 100ms
// 300ms
Stagger
does not render anything by itself. Instead, it maintains a rendering
delay across elements and passes it to the render function.
The Stagger
component can be abstracted with another component that handles
the actual animation:
const Appear = ({ children, in, delay = 100 }) =>
<Stagger in={in} delay={delay}>
{({ value, delay }) =>
<div
style={{
opacity: value ? 1 : 0,
transition: `opacity 300ms ${delay}ms`,
}}
>
{children}
</div>
}
</Stagger>
You can combine Stagger
similarly with most React animation libraries,
including react-transition-group
and
react-motion
.
Stagger can be used anywhere in the component tree:
const ImageGallery = images => (
<section>
{images.map(image => (
<Appear>
<img src={image.src} alt={image.alt} />
</Appear>
))}
</section>
)
const Page = ({title, subtitle, images}) => (
<article>
<Appear>
<h1>{title}</h1>
</Appear>
<Appear>
<p>{subtitle}</p>
</Appear>
<ImageGallery images={images} />
</article>
)
In this case, the title, subtitle and each image fades in, 100ms apart.
There are two key features worth expanding on; nesting and delay collapse.
By wrapping a group of Stagger
elements in a Stagger
element higher in the
render tree, a few possibilities open up:
- Control the appearance of a whole tree of staggered elements.
- Set a delay around a group of elements.
const Page = ({ title, subtitle, images, isReady }) =>
{/* Start staggering only when the page is ready */}
<Stagger in={isReady}>
<article>
<Appear>
<h1>{title}</h1>
</Appear>
<Appear>
<p>{subtitle}</p>
</Appear>
{/* Delay whole image gallery group by 500ms. */}
<Stagger delay={500}>
<ImageGallery images={images} />
</Stagger>
</article>
</Stagger>
By combining react-stagger
with
react-intersection-observer
or another
scroll observer, you can make elements appear with stagger as you scroll down
the page.
import Observer from 'react-intersection-observer'
const ScrollStagger = ({children}) => (
<Observer triggerOnce rootMargin="10vh">
{inView => <Stagger in={inView}>{children}</Stagger>}
</Observer>
)
const PageSection = ({title, subtitle, images}) => (
<ScrollStagger>
<section>
<Appear>
<h1>{title}</h1>
</Appear>
<Appear>
<p>{subtitle}</p>
</Appear>
<ImageGallery images={images} />
</section>
</ScrollStagger>
)
Delay in React Stagger works a bit like css margins. The delay is applied before and after the element. All delay between two "leaf" Stagger elements collapses, so the biggest delay wins.
const renderDelay = title => ({ delay }) => <div>{title} = {delay}ms</div>
render(
<>
<Stagger delay={500}>{renderDelay('title')}</Stagger>
<Stagger>{renderDelay('subtitle')}</Stagger>
<Stagger>{renderDelay('body')}</Stagger>
<Stagger delay={500}>
<Stagger>{renderDelay('image')}</Stagger>
<Stagger>{renderDelay('image')}</Stagger>
<Stagger>{renderDelay('image')}</Stagger>
</Stagger>
<Stagger>{renderDelay('footer')}
</>,
document.getElementById('root'),
)
// Renders: | Explanation:
// title = 0ms | first delay collapses
// subtitle = 500ms | max(500ms (title), 100ms (subtitle))
// body = 600ms | max(100ms (subtitle), 100ms (body))
// image = 1100ms | max(100ms (body), 500ms (image parent), 100ms (image))
// image = 1200ms | ...
// image = 1300ms | ...
// footer = 1800ms | max(100ms (image), 500ms (image parent), 100ms (footer))
Thanks goes to these wonderful people (emoji key):
Eiríkur Heiðar Nilsson 💻 📖 🚇 |
---|
This project follows the all-contributors specification. Contributions of any kind welcome!