├── README.md ├── Sticker.js ├── Sticky.js ├── index.js └── package.json /README.md: -------------------------------------------------------------------------------- 1 | ## React Sticker 2 | Creates instagram style sticky headers that collide when one header scrolls into another. 3 | 4 | If you need something like this, but this does not work out of the box for you, file an issue and I will work to add configuration options to support a wider set of use cases. 5 | 6 | **Note:** This does not follow idiomatic react. The primary goal of this project is to be performant during scrolling and clean to implement. 7 | 8 | **Example** 9 | [react-sticker-example](https://github.com/rt2zz/react-sticker-example) 10 | 11 | ### Usage 12 | Create a parent component and put your sticky headers inside of it. 13 | 14 | *general* 15 | ```js 16 | 17 | ... 18 | ... 19 | 20 | ``` 21 | 22 | *example* 23 | ```js 24 | var Sticker = require('react-sticker') 25 | var Sticky = Sticker.Sticky 26 | 27 | var jsx = ( 28 | 29 |
30 |

First Sticky

31 |
Contents
32 |
33 |
34 |

Second Sticky

35 |
Contents
36 |
37 |
38 |

Third Sticky

39 |
Contents
40 |
41 |
42 | ) 43 | ``` 44 | 45 | When the Sticker container is scrolled such that the Sticky header reaches the top, the position of the Sticky element will be fixed (i.e. stuck) to the top of the screen. When the top of the second header hits the first header it will begin to push the first header out of the way, and then become sticky itself. 46 | 47 | Multiple independent components will work as siblings but not if nested. 48 | 49 | ### Styling & Scrolling 50 | If you set `useWindow={true}` react-sticker should work out of the box. Otherwise you will need to make your sticker container "scrollable" with css, e.g. `` 51 | -------------------------------------------------------------------------------- /Sticker.js: -------------------------------------------------------------------------------- 1 | var React = require('react') 2 | 3 | /*@TODO 4 | Add configuration props e.g. 5 | * Re-sticky header on scroll down (when unset) 6 | * Stuck class 7 | * Default styles 8 | 9 | Improvements 10 | * Make offset containers work better (right now it does not clip the header) 11 | * Bug when scrolling a container inside of an already scrolled container 12 | */ 13 | 14 | module.exports = React.createClass({ 15 | 16 | propTypes: { 17 | useWindow: React.PropTypes.bool, 18 | }, 19 | 20 | getDefaultProps: function() { 21 | return { 22 | useWindow: false 23 | } 24 | }, 25 | 26 | componentWillMount: function(){ 27 | // this.handleScroll = require('lodash').throttle(this.handleScroll, 100) 28 | }, 29 | 30 | componentDidMount: function() { 31 | this.initialize() 32 | if(this.props.useWindow){ 33 | window.addEventListener('scroll', this.handleScroll) 34 | window.addEventListener('resize', this.initialize) 35 | } 36 | }, 37 | 38 | componentDidUpdate: function() { 39 | this.initialize() 40 | }, 41 | 42 | componentWillUnmount: function() { 43 | window.removeEventListener('scroll', this.handleScroll); 44 | window.removeEventListener('resize', this.initialize); 45 | }, 46 | 47 | initialize: function(){ 48 | this._scrollNode = this.props.useWindow ? document.body : this.getDOMNode() 49 | this.findStickies() 50 | this.findCursor() 51 | this.setStuck() 52 | }, 53 | 54 | findStickies: function(){ 55 | var node = this._scrollNode 56 | this._stickies = node.querySelectorAll('[data-sticky]') 57 | }, 58 | 59 | findCursor: function(){ 60 | var node = this._scrollNode 61 | var stickies = this._stickies 62 | 63 | //find the current stuck and next sticky elements 64 | if(stickies[0].offsetTop > node.scrollTop){ 65 | this._cursor = -1 66 | } 67 | else{ 68 | var i = 0 69 | while(true){ 70 | var next = stickies[i+1] 71 | if(next.offsetTop > node.scrollTop){ 72 | this._cursor = i 73 | break 74 | } 75 | else{ 76 | i++ 77 | if(i+1 === stickies.length) break 78 | } 79 | } 80 | } 81 | this.setNodes() 82 | }, 83 | 84 | setNodes: function(){ 85 | this._stuck = this._stickies[this._cursor] 86 | this._next = this._stickies[this._cursor+1] || null 87 | }, 88 | 89 | setStuck: function(){ 90 | //remove last stuck and placeholder 91 | this.unset() 92 | this.setNodes() 93 | 94 | if(this._stuck){ 95 | //insert placeholder 96 | this.insertPlaceholder(this._stuck) 97 | this.makeSticky(this._stuck) 98 | } 99 | }, 100 | 101 | unset: function(){ 102 | if(this._stuck){ 103 | this._stuck.style.position = 'inherit' 104 | this._stuck.style.top = null 105 | this._stuck.classList.remove('stuck') 106 | if(this._placeHolder) this._stuck.parentNode.removeChild(this._placeHolder) 107 | this._placeHolder = null 108 | this._stuck = null 109 | } 110 | this._next = null 111 | }, 112 | 113 | insertPlaceholder: function(stuck){ 114 | this._placeHolder = document.createElement("div") 115 | this._placeHolder.style.width = stuck.offsetWidth+'px' 116 | this._placeHolder.style.height = stuck.offsetHeight+'px' 117 | stuck.parentNode.insertBefore(this._placeHolder, stuck) 118 | }, 119 | 120 | makeSticky: function (stuck){ 121 | stuck.style.width = stuck.offsetWidth+'px' 122 | stuck.style.height = stuck.offsetHeight+'px' 123 | stuck.style.top = this._scrollNode.offsetTop 124 | stuck.style.position = 'fixed' 125 | stuck.classList.add('stuck') 126 | }, 127 | 128 | /* 129 | This handler does as little work as possible, checking if 130 | a) the scrollTop has hit our up or down boundaries 131 | -> Change cursor and update sticky state 132 | b) if nothing is "set" and we are scrolling down 133 | -> Update boundaries 134 | */ 135 | handleScroll: function(){ 136 | var node = this._scrollNode 137 | var downBoundary = this._next && this._next.offsetTop 138 | var upBoundary = this._placeHolder && this._placeHolder.offsetTop 139 | var set = (this._cursor === -1 || upBoundary) ? true : false 140 | var relScrollTop = node.offsetTop + node.scrollTop 141 | 142 | //If we are not set, reset cursor on next downward scroll. This will get called once to set this new up/down boundaries 143 | if(!set && this._lastScrollTop < relScrollTop){ 144 | this.findCursor() 145 | //If we have crossed our downward boundary, make sticky 146 | if(downBoundary && downBoundary < node.scrollTop) this.setStuck(true) 147 | return 148 | } 149 | this._lastScrollTop = relScrollTop 150 | 151 | //Check if we have hit our boundaries and change the cursor and stuck state accordingly 152 | if(downBoundary && relScrollTop >= downBoundary){ 153 | this._cursor++ 154 | this.setStuck(true) 155 | return 156 | } 157 | if(upBoundary && relScrollTop <= upBoundary){ 158 | this._cursor-- 159 | this.unset() 160 | return 161 | } 162 | //Check for Sticky collision and adjust top position accordingly 163 | if(set && this._stuck && relScrollTop >= downBoundary - this._stuck.offsetHeight){ 164 | var top = Math.min(node.offsetTop, this._next.offsetTop - node.scrollTop - this._stuck.offsetHeight) 165 | this._stuck.style.top = top+'px' 166 | return 167 | } 168 | 169 | //If none of the above conditions triggered, check if we are out of bounds, and if so reset cursor 170 | if(relScrollTop > downBoundary || relScrollTop < upBoundary){ 171 | this.findCursor() 172 | } 173 | 174 | }, 175 | 176 | render: function() { 177 | var stickerOnScroll = this.props.useWindow ? null : this.handleScroll 178 | return( 179 |
180 | {this.props.children} 181 |
182 | ) 183 | } 184 | }); 185 | -------------------------------------------------------------------------------- /Sticky.js: -------------------------------------------------------------------------------- 1 | var React = require('react') 2 | 3 | var _ = require('lodash') 4 | 5 | module.exports = React.createClass({ 6 | 7 | getDefaultProps: function(){ 8 | return { 9 | zIndex: 1 10 | } 11 | }, 12 | 13 | render: function() { 14 | return( 15 |
16 |
17 | {this.props.children} 18 |
19 |
20 | ) 21 | } 22 | }); 23 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./Sticker.js') 2 | module.exports.Sticky = require('./Sticky.js') 3 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-sticker", 3 | "version": "1.0.4", 4 | "description": "Creates instagram style sticky headers that collide when one header scrolls into another.", 5 | "main": "index.js", 6 | "dependencies": { 7 | "lodash": "^2.4.1" 8 | }, 9 | "devDependencies": {}, 10 | "scripts": { 11 | "test": "echo \"Error: no test specified\" && exit 1" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git://github.com/rt2zz/react-sticker.git" 16 | }, 17 | "author": "rt2zz ", 18 | "license": "MIT", 19 | "keywords": [ 20 | "react", 21 | "sticky", 22 | "component", 23 | "react-component" 24 | ] 25 | } 26 | --------------------------------------------------------------------------------