├── .gitattributes ├── .npmrc ├── .gitignore ├── CHANGELOG.md ├── .travis.yml ├── .all-contributorsrc ├── src ├── StaggerTiming.js ├── index.js └── __tests__ │ └── index.js ├── LICENSE ├── package.json ├── CODE_OF_CONDUCT.md └── README.md /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | *.js text eol=lf 3 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=http://registry.npmjs.org/ 2 | package-lock=false 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | dist 4 | .DS_Store 5 | .eslintcache 6 | yarn.lock 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | The changelog is automatically updated using [semantic-release](https://github.com/semantic-release/semantic-release). 4 | You can see it on the [releases page](../../releases). 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - v9 4 | - v8 5 | sudo: false 6 | cache: 7 | directories: 8 | - ~/.npm 9 | notifications: 10 | email: false 11 | install: npm install 12 | script: npm run validate 13 | after_success: kcd-scripts travis-after-success 14 | branches: 15 | only: master 16 | -------------------------------------------------------------------------------- /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "projectName": "react-stagger", 3 | "projectOwner": "aranja", 4 | "repoType": "github", 5 | "files": [ 6 | "README.md" 7 | ], 8 | "imageSize": 100, 9 | "commit": true, 10 | "contributors": [ 11 | { 12 | "login": "eirikurn", 13 | "name": "Eiríkur Heiðar Nilsson", 14 | "avatar_url": "https://avatars2.githubusercontent.com/u/115094?v=4", 15 | "profile": "https://aranja.com", 16 | "contributions": [ 17 | "code", 18 | "doc", 19 | "infra", 20 | "test" 21 | ] 22 | } 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /src/StaggerTiming.js: -------------------------------------------------------------------------------- 1 | const STAGGER_GROUP_WINDOW = 100 2 | 3 | class StaggerTiming { 4 | lastStagger = 0 5 | currentDelay = 0 6 | lastDelay = 0 7 | 8 | getDelay(newDelay, commitDelay) { 9 | const now = Date.now() 10 | newDelay = +newDelay 11 | if (now - this.lastStagger > STAGGER_GROUP_WINDOW) { 12 | this.lastStagger = now 13 | this.currentDelay = 0 14 | this.lastDelay = 0 15 | } 16 | 17 | if (this.currentDelay > 0 || this.lastDelay > 0) { 18 | this.lastDelay = Math.max(this.lastDelay, newDelay) 19 | } 20 | 21 | const delay = this.currentDelay + this.lastDelay 22 | if (commitDelay) { 23 | this.currentDelay += this.lastDelay 24 | this.lastDelay = newDelay 25 | } 26 | return delay 27 | } 28 | } 29 | 30 | export default StaggerTiming 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Aranja (https://aranja.com) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-stagger", 3 | "version": "0.0.0-semantically-released", 4 | "description": "React component for staggered rendering.", 5 | "main": "dist/react-stagger.cjs.js", 6 | "jsnext:main": "dist/react-stagger.esm.js", 7 | "module": "dist/react-stagger.esm.js", 8 | "engines": { 9 | "node": ">=8", 10 | "npm": ">=5" 11 | }, 12 | "scripts": { 13 | "build": "kcd-scripts build --bundle", 14 | "lint": "kcd-scripts lint", 15 | "test": "kcd-scripts test", 16 | "test:update": "npm test -- --updateSnapshot --coverage", 17 | "validate": "kcd-scripts validate", 18 | "setup": "npm install && npm run validate -s", 19 | "precommit": "kcd-scripts precommit" 20 | }, 21 | "files": ["dist"], 22 | "keywords": [], 23 | "author": { 24 | "name": "Aranja", 25 | "email": "aranja@aranja.com", 26 | "url": "https://aranja.com" 27 | }, 28 | "license": "MIT", 29 | "devDependencies": { 30 | "kcd-scripts": "^0.36.1", 31 | "prop-types": "^15.6.1", 32 | "react": "^16.3.1", 33 | "react-test-renderer": "^16.3.1" 34 | }, 35 | "peerDependencies": { 36 | "prop-types": ">=15", 37 | "react": ">=15" 38 | }, 39 | "eslintIgnore": ["node_modules", "coverage", "dist"], 40 | "repository": { 41 | "type": "git", 42 | "url": "https://github.com/aranja/react-stagger.git" 43 | }, 44 | "bugs": { 45 | "url": "https://github.com/aranja/react-stagger/issues" 46 | }, 47 | "homepage": "https://github.com/aranja/react-stagger#readme" 48 | } 49 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | education, socio-economic status, nationality, personal appearance, race, 10 | religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at [aranja@aranja.com](mailto:aranja@aranja.com). All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [https://www.contributor-covenant.org/version/1/4/code-of-conduct.html](https://www.contributor-covenant.org/version/1/4/code-of-conduct.html) 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import {Component} from 'react' 2 | import PropTypes from 'prop-types' 3 | import StaggerTiming from './StaggerTiming' 4 | 5 | const globalTiming = new StaggerTiming() 6 | 7 | class Stagger extends Component { 8 | static childContextTypes = { 9 | stagger: PropTypes.object.isRequired, 10 | } 11 | 12 | static contextTypes = { 13 | stagger: PropTypes.object, 14 | } 15 | 16 | static defaultProps = { 17 | delay: 100, 18 | in: true, 19 | appear: true, 20 | } 21 | 22 | static propTypes = { 23 | timing: PropTypes.instanceOf(StaggerTiming), 24 | delay: PropTypes.oneOfType([ 25 | PropTypes.number, 26 | PropTypes.arrayOf(PropTypes.number), 27 | ]), 28 | in: PropTypes.bool, 29 | appear: PropTypes.bool, 30 | children: PropTypes.oneOfType([PropTypes.node, PropTypes.func]), 31 | } 32 | 33 | staggerContext = { 34 | subscribe: this.subscribe.bind(this), 35 | value: false, 36 | timing: this.props.timing || this.context.timing || globalTiming, 37 | } 38 | selfValue = this.props.in 39 | subscribers = [] 40 | unsubscribe = null 41 | state = { 42 | value: false, 43 | delay: 0, 44 | } 45 | 46 | componentWillMount() { 47 | if (this.context.stagger) { 48 | this.unsubscribe = this.context.stagger.subscribe(() => 49 | this.checkUpdate(), 50 | ) 51 | } 52 | if (!this.props.appear) { 53 | this.checkUpdate(true) 54 | } 55 | } 56 | 57 | componentDidMount() { 58 | if (this.props.appear) { 59 | this.checkUpdate() 60 | } 61 | } 62 | 63 | componentWillReceiveProps(newProps) { 64 | this.selfValue = newProps.in 65 | this.checkUpdate() 66 | } 67 | 68 | componentWillUnmount() { 69 | if (this.unsubscribe) { 70 | this.unsubscribe() 71 | } 72 | } 73 | 74 | checkUpdate(forceInstant) { 75 | // Only stagger if self and all parents are active. 76 | const parentValue = this.context.stagger ? this.context.stagger.value : true 77 | const value = this.selfValue && parentValue 78 | 79 | // Only continue if the value has changed. 80 | if (value === this.staggerContext.value) { 81 | return 82 | } 83 | 84 | this.staggerContext.value = value 85 | 86 | const delay = this.calculateDelay(value, forceInstant) 87 | this.setState({ 88 | value, 89 | delay, 90 | }) 91 | } 92 | 93 | calculateDelay(value, forceInstant) { 94 | // Get delay for self. Note, the actual delay is max of all stagger 95 | // delays since last leaf stagger. 96 | // stagger(300) - 0 97 | // stagger(100) - 0 98 | // stagger(100) - 100 99 | // stagger(100) - 200 100 | // stagger(200) - 400 101 | // stagger(100) - 400 102 | // stagger(500) - 900 103 | 104 | if (forceInstant) { 105 | return 0 106 | } 107 | 108 | const timing = this.staggerContext.timing 109 | const isLeaf = !this.subscribers.length 110 | const [beforeDelay, afterDelay] = this.getOwnDelay() 111 | 112 | // Add delay for self and get total delay since last leaf. 113 | const totalDelay = value ? timing.getDelay(beforeDelay, isLeaf) : 0 114 | 115 | if (!isLeaf) { 116 | // Notify children of change. 117 | this.subscribers.forEach(subscriber => { 118 | subscriber() 119 | }) 120 | } 121 | 122 | // Add delay after children. 123 | if (value) { 124 | timing.getDelay(afterDelay, false) 125 | } 126 | 127 | return totalDelay 128 | } 129 | 130 | getOwnDelay() { 131 | const {delay} = this.props 132 | return Array.isArray(delay) ? delay : [delay, delay] 133 | } 134 | 135 | getChildContext() { 136 | return { 137 | stagger: this.staggerContext, 138 | } 139 | } 140 | 141 | subscribe(handler) { 142 | this.subscribers.push(handler) 143 | return () => { 144 | this.subscribers = this.subscribers.filter(h => h !== handler) 145 | } 146 | } 147 | 148 | render() { 149 | const {children} = this.props 150 | if (typeof children === 'function') { 151 | return children(this.state) 152 | } 153 | return children || null 154 | } 155 | } 156 | 157 | export default Stagger 158 | -------------------------------------------------------------------------------- /src/__tests__/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import TestRenderer from 'react-test-renderer' 3 | import Stagger from '..' 4 | import StaggerTiming from '../StaggerTiming' 5 | 6 | describe('Stagger', () => { 7 | let timing 8 | beforeEach(() => { 9 | timing = new StaggerTiming() 10 | }) 11 | 12 | it('should default to true', () => { 13 | const spy = jest.fn().mockReturnValue(null) 14 | TestRenderer.create({spy}) 15 | expect(spy).lastCalledWith({value: true, delay: 0}) 16 | }) 17 | 18 | it('should forward false value', () => { 19 | const spy = jest.fn().mockReturnValue(null) 20 | TestRenderer.create( 21 | 22 | {spy} 23 | , 24 | ) 25 | expect(spy).lastCalledWith({value: false, delay: 0}) 26 | }) 27 | 28 | it('should forward true value', () => { 29 | const spy = jest.fn().mockReturnValue(null) 30 | TestRenderer.create( 31 | 32 | {spy} 33 | , 34 | ) 35 | expect(spy).lastCalledWith({value: true, delay: 0}) 36 | }) 37 | 38 | it('should first render false', () => { 39 | const spy = jest.fn().mockReturnValue(null) 40 | TestRenderer.create( 41 | 42 | {spy} 43 | , 44 | ) 45 | expect(spy.mock.calls[0]).toEqual([{value: false, delay: 0}]) 46 | expect(spy.mock.calls[1]).toEqual([{value: true, delay: 0}]) 47 | }) 48 | 49 | it('when appear is false, initial render should match in value', () => { 50 | const spy = jest.fn().mockReturnValue(null) 51 | TestRenderer.create( 52 | 53 | {spy} 54 | , 55 | ) 56 | expect(spy.mock.calls[0]).toEqual([{value: true, delay: 0}]) 57 | }) 58 | 59 | it('should only be true if all ancestors are true', () => { 60 | const spy = jest.fn().mockReturnValue(null) 61 | TestRenderer.create( 62 | 63 | {spy} 64 | , 65 | ) 66 | expect(spy).lastCalledWith({value: false, delay: 0}) 67 | }) 68 | 69 | it('should stagger multiple elements', () => { 70 | const spy = jest.fn().mockReturnValue(null) 71 | TestRenderer.create( 72 |
73 | {spy} 74 | {spy} 75 | {spy} 76 | {spy} 77 |
, 78 | ) 79 | expect(spy.mock.calls.slice(-4)).toEqual([ 80 | [{value: true, delay: 0}], 81 | [{value: true, delay: 100}], 82 | [{value: true, delay: 200}], 83 | [{value: true, delay: 300}], 84 | ]) 85 | }) 86 | 87 | it('should delay on both sides', () => { 88 | const spy = jest.fn().mockReturnValue(null) 89 | TestRenderer.create( 90 |
91 | {spy} 92 | {spy} 93 | 94 | {spy} 95 | 96 | {spy} 97 | {spy} 98 |
, 99 | ) 100 | expect(spy.mock.calls.slice(-5)).toEqual([ 101 | [{value: true, delay: 0}], 102 | [{value: true, delay: 100}], 103 | [{value: true, delay: 400}], 104 | [{value: true, delay: 700}], 105 | [{value: true, delay: 800}], 106 | ]) 107 | }) 108 | 109 | it('supports before and after delay', () => { 110 | const spy = jest.fn().mockReturnValue(null) 111 | TestRenderer.create( 112 |
113 | {spy} 114 | 115 | {spy} 116 | 117 | {spy} 118 |
, 119 | ) 120 | expect(spy.mock.calls.slice(-3)).toEqual([ 121 | [{value: true, delay: 0}], 122 | [{value: true, delay: 200}], 123 | [{value: true, delay: 500}], 124 | ]) 125 | }) 126 | 127 | it('should collapse starting delays', () => { 128 | const spy = jest.fn().mockReturnValue(null) 129 | TestRenderer.create( 130 |
131 | 132 | {spy} 133 | 134 |
, 135 | ) 136 | expect(spy).lastCalledWith({value: true, delay: 0}) 137 | }) 138 | 139 | it('should collapse non-leaf delays', () => { 140 | const spy = jest.fn().mockReturnValue(null) 141 | TestRenderer.create( 142 |
143 | {spy} 144 | 145 | 146 | {spy} 147 | 148 | 149 | {spy} 150 |
, 151 | ) 152 | expect(spy.mock.calls.slice(-3)).toEqual([ 153 | [{value: true, delay: 0}], 154 | [{value: true, delay: 300}], 155 | [{value: true, delay: 600}], 156 | ]) 157 | }) 158 | 159 | it('should reset with no delay', () => { 160 | const spy = jest.fn().mockReturnValue(null) 161 | const renderer = TestRenderer.create( 162 |
163 | {spy} 164 | 165 | {spy} 166 | 167 | {spy} 168 |
, 169 | ) 170 | renderer.update( 171 |
172 | 173 | {spy} 174 | 175 | 176 | {spy} 177 | 178 | 179 | {spy} 180 | 181 |
, 182 | ) 183 | 184 | expect(spy.mock.calls.slice(-3)).toEqual([ 185 | [{value: false, delay: 0}], 186 | [{value: false, delay: 0}], 187 | [{value: false, delay: 0}], 188 | ]) 189 | }) 190 | 191 | it('should unsubscribe when unmounted', () => { 192 | const renderer = TestRenderer.create( 193 | 194 | 195 | , 196 | ) 197 | renderer.update() 198 | 199 | const rootInstance = renderer.root.instance 200 | expect(rootInstance.subscribers).toHaveLength(0) 201 | }) 202 | 203 | it('can unmount without error', () => { 204 | const renderer = TestRenderer.create() 205 | const rootInstance = renderer.root.instance 206 | expect(rootInstance.unsubscribe).toBeNull() 207 | 208 | renderer.update(null) 209 | }) 210 | }) 211 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

react-stagger

3 | 4 |

React component for staggered rendering.

5 |
6 | 7 |
8 | 9 | [![Build Status][build-badge]][build] 10 | [![Code Coverage][coverage-badge]][coverage] 11 | [![version][version-badge]][package] 12 | [![downloads][downloads-badge]][npmtrends] 13 | [![MIT License][license-badge]][license] 14 | 15 | [![PRs Welcome][prs-badge]][prs] 16 | [![All Contributors][contributers-badge]](#contributors) 17 | [![Code of Conduct][coc-badge]][coc] 18 | 19 | [![Watch on GitHub][github-watch-badge]][github-watch] 20 | [![Star on GitHub][github-star-badge]][github-star] 21 | [![Tweet][twitter-badge]][twitter] 22 | 23 | ## The problem 24 | 25 | When building websites and apps with designers, we want things to flow 26 | smoothly. This often involves making things appear with a staggering effect, 27 | i.e. one at a time. 28 | 29 | Doing this in React can be tricky. React encourages component isolation which 30 | can make it difficult to coordinate animation across components, especially 31 | when they are deeply nested. 32 | 33 | ## This solution 34 | 35 | React Stagger provides a low-level Transition-like `Stagger` component that 36 | calculates a rendering delay based on other Stagger instances. 37 | 38 | ## Table of Contents 39 | 40 | 41 | 42 | 43 | 44 | * [Installation](#installation) 45 | * [Usage](#usage) 46 | * [Nesting](#nesting) 47 | * [Stagger on scroll](#stagger-on-scroll) 48 | * [Advanced: Delay collapse](#advanced-delay-collapse) 49 | * [Inspiration](#inspiration) 50 | * [LICENSE](#license) 51 | * [Contributors](#contributors) 52 | 53 | 54 | 55 | ## Installation 56 | 57 | This module is distributed via [npm][npm] which is bundled with [node][node] and 58 | should be installed as one of your project's `dependencies`: 59 | 60 | ```bash 61 | npm install --save react-stagger 62 | ``` 63 | 64 | ## Usage 65 | 66 | ```javascript 67 | import React from 'react' 68 | import {render} from 'react-dom' 69 | import Stagger from 'react-stagger' 70 | 71 | render( 72 | <> 73 | {({delay}) =>

{delay}ms

}
74 | {({delay}) =>

{delay}ms

}
75 | {({delay}) =>

{delay}ms

}
76 | , 77 | document.getElementById('root'), 78 | ) 79 | 80 | // Renders: 81 | // 0ms 82 | // 100ms 83 | // 300ms 84 | ``` 85 | 86 | `Stagger` does not render anything by itself. Instead, it maintains a rendering 87 | delay across elements and passes it to the render function. 88 | 89 | The `Stagger` component can be abstracted with another component that handles 90 | the actual animation: 91 | 92 | ```javascript 93 | const Appear = ({ children, in, delay = 100 }) => 94 | 95 | {({ value, delay }) => 96 |
102 | {children} 103 |
104 | } 105 |
106 | ``` 107 | 108 | You can combine `Stagger` similarly with most React animation libraries, 109 | including [`react-transition-group`][react-transition-group] and 110 | [`react-motion`][react-motion]. 111 | 112 | Stagger can be used anywhere in the component tree: 113 | 114 | ```javascript 115 | const ImageGallery = images => ( 116 |
117 | {images.map(image => ( 118 | 119 | {image.alt} 120 | 121 | ))} 122 |
123 | ) 124 | 125 | const Page = ({title, subtitle, images}) => ( 126 |
127 | 128 |

{title}

129 |
130 | 131 |

{subtitle}

132 |
133 | 134 |
135 | ) 136 | ``` 137 | 138 | In this case, the title, subtitle and each image fades in, 100ms 139 | apart. 140 | 141 | There are two key features worth expanding on; nesting and delay collapse. 142 | 143 | ### Nesting 144 | 145 | By wrapping a group of `Stagger` elements in a `Stagger` element higher in the 146 | render tree, a few possibilities open up: 147 | 148 | * Control the appearance of a whole tree of staggered elements. 149 | * Set a delay around a group of elements. 150 | 151 | ```javascript 152 | const Page = ({ title, subtitle, images, isReady }) => 153 | {/* Start staggering only when the page is ready */} 154 | 155 |
156 | 157 |

{title}

158 |
159 | 160 |

{subtitle}

161 |
162 | 163 | {/* Delay whole image gallery group by 500ms. */} 164 | 165 | 166 | 167 |
168 |
169 | ``` 170 | 171 | ### Stagger on scroll 172 | 173 | By combining `react-stagger` with 174 | [`react-intersection-observer`][react-intersection-observer] or another 175 | scroll observer, you can make elements appear with stagger as you scroll down 176 | the page. 177 | 178 | ```javascript 179 | import Observer from 'react-intersection-observer' 180 | 181 | const ScrollStagger = ({children}) => ( 182 | 183 | {inView => {children}} 184 | 185 | ) 186 | 187 | const PageSection = ({title, subtitle, images}) => ( 188 | 189 |
190 | 191 |

{title}

192 |
193 | 194 |

{subtitle}

195 |
196 | 197 |
198 |
199 | ) 200 | ``` 201 | 202 | ### Advanced: Delay collapse 203 | 204 | Delay in React Stagger works a bit like css margins. The delay is applied 205 | before and after the element. All delay between two "leaf" Stagger elements 206 | collapses, so the biggest delay wins. 207 | 208 | ```javascript 209 | const renderDelay = title => ({ delay }) =>
{title} = {delay}ms
210 | 211 | render( 212 | <> 213 | {renderDelay('title')} 214 | {renderDelay('subtitle')} 215 | {renderDelay('body')} 216 | 217 | 218 | {renderDelay('image')} 219 | {renderDelay('image')} 220 | {renderDelay('image')} 221 | 222 | 223 | {renderDelay('footer')} 224 | , 225 | document.getElementById('root'), 226 | ) 227 | 228 | // Renders: | Explanation: 229 | // title = 0ms | first delay collapses 230 | // subtitle = 500ms | max(500ms (title), 100ms (subtitle)) 231 | // body = 600ms | max(100ms (subtitle), 100ms (body)) 232 | // image = 1100ms | max(100ms (body), 500ms (image parent), 100ms (image)) 233 | // image = 1200ms | ... 234 | // image = 1300ms | ... 235 | // footer = 1800ms | max(100ms (image), 500ms (image parent), 100ms (footer)) 236 | ``` 237 | 238 | ## Inspiration 239 | 240 | * [react-transition-group][react-transition-group] 241 | 242 | ## LICENSE 243 | 244 | [MIT][license] 245 | 246 | [build-badge]: https://img.shields.io/travis/aranja/react-stagger.svg?style=flat-square 247 | [build]: https://travis-ci.org/aranja/react-stagger 248 | [coc-badge]: https://img.shields.io/badge/code%20of-conduct-ff69b4.svg?style=flat-square 249 | [coc]: https://github.com/aranja/react-stagger/blob/master/CODE_OF_CONDUCT.md 250 | [contributers]: #contributors 251 | [contributers-badge]: https://img.shields.io/badge/all_contributors-1-orange.svg?style=flat-square 252 | [coverage-badge]: https://img.shields.io/codecov/c/github/aranja/react-stagger.svg?style=flat-square 253 | [coverage]: https://codecov.io/github/aranja/react-stagger 254 | [donate-badge]: https://img.shields.io/badge/$-support-green.svg?style=flat-square 255 | [downloads-badge]: https://img.shields.io/npm/dm/react-stagger.svg?style=flat-square 256 | [github-star-badge]: https://img.shields.io/github/stars/aranja/react-stagger.svg?style=social 257 | [github-star]: https://github.com/aranja/react-stagger/stargazers 258 | [github-watch-badge]: https://img.shields.io/github/watchers/aranja/react-stagger.svg?style=social 259 | [github-watch]: https://github.com/aranja/react-stagger/watchers 260 | [license-badge]: https://img.shields.io/npm/l/react-stagger.svg?style=flat-square 261 | [license]: https://github.com/aranja/react-stagger/blob/master/LICENSE 262 | [node]: https://nodejs.org 263 | [npm]: https://www.npmjs.com/ 264 | [npmtrends]: http://www.npmtrends.com/react-stagger 265 | [package]: https://www.npmjs.com/package/react-stagger 266 | [prs-badge]: https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square 267 | [prs]: https://github.com/aranja/react-stagger/issues 268 | [react-intersection-observer]: https://www.npmjs.com/package/react-intersection-observer 269 | [react-transition-group]: https://reactcommunity.org/react-transition-group 270 | [react-motion]: https://github.com/chenglou/react-motion 271 | [twitter-badge]: https://img.shields.io/twitter/url/https/github.com/aranja/react-stagger.svg?style=social 272 | [twitter]: https://twitter.com/intent/tweet?text=Check%20out%20react-stagger%20by%20%40aranjastudio%20https%3A%2F%2Fgithub.com%2Faranja%2Freact-stagger%20%F0%9F%91%8D 273 | [version-badge]: https://img.shields.io/npm/v/react-stagger.svg?style=flat-square 274 | 275 | ## Contributors 276 | 277 | Thanks goes to these wonderful people ([emoji key](https://github.com/kentcdodds/all-contributors#emoji-key)): 278 | 279 | 280 | 281 | 282 | | [
Eiríkur Heiðar Nilsson](https://aranja.com)
[💻](https://github.com/aranja/react-stagger/commits?author=eirikurn "Code") [📖](https://github.com/aranja/react-stagger/commits?author=eirikurn "Documentation") [🚇](#infra-eirikurn "Infrastructure (Hosting, Build-Tools, etc)") [⚠️](https://github.com/aranja/react-stagger/commits?author=eirikurn "Tests") | 283 | | :---: | 284 | 285 | 286 | 287 | This project follows the [all-contributors](https://github.com/kentcdodds/all-contributors) specification. Contributions of any kind welcome! 288 | --------------------------------------------------------------------------------