├── .gitignore ├── .editorconfig ├── rollup.config.js ├── package.json ├── license ├── src └── index.js └── readme.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | *.lock 4 | *.log 5 | dist 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.{json,yml,md}] 12 | indent_style = space 13 | indent_size = 2 14 | 15 | [*.md] 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import buble from 'rollup-plugin-buble'; 2 | 3 | export default { 4 | entry: 'src/index.js', 5 | dest: 'dist/preact-scroll-header.js', 6 | moduleName: 'ScrollHeader', 7 | external: ['preact'], 8 | format: 'umd', 9 | plugins: [ 10 | buble({ 11 | jsx: 'h', 12 | transforms: { 13 | modules: false 14 | } 15 | }) 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "preact-scroll-header", 3 | "version": "1.0.3", 4 | "module": "src/index.js", 5 | "main": "dist/preact-scroll-header.js", 6 | "repository": "lukeed/preact-scroll-header", 7 | "description": "A (800b gzip) header that will show/hide while scrolling", 8 | "license": "MIT", 9 | "files": [ 10 | "src", 11 | "dist" 12 | ], 13 | "author": { 14 | "name": "Luke Edwards", 15 | "email": "luke.edwards05@gmail.com", 16 | "url": "https://lukeed.com" 17 | }, 18 | "scripts": { 19 | "build": "rollup -c", 20 | "postbuild": "uglifyjs dist/preact-scroll-header.js -c -m -o dist/preact-scroll-header.min.js" 21 | }, 22 | "devDependencies": { 23 | "rollup": "^0.41.4", 24 | "rollup-plugin-buble": "^0.15.0", 25 | "uglify-js": "^2.7.5" 26 | }, 27 | "peerDependencies": { 28 | "preact": "^7.2.0" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Luke Edwards (https://lukeed.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 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import { h, Component } from 'preact'; 2 | 3 | function noop() {} 4 | 5 | function toNode(el) { 6 | return el === document.body ? document : el; 7 | } 8 | 9 | let lastScroll, firstReverse; 10 | export default class ScrollHeader extends Component { 11 | constructor(props) { 12 | super(props); 13 | 14 | this.height = 0; 15 | this.buffer = props.buffer || 0; 16 | this.parent = props.listenTo || null; 17 | 18 | this.state = { 19 | isFixed: false, 20 | isReady: false, 21 | isShown: false 22 | }; 23 | 24 | this.onScroll = e => { 25 | const Y = (e.target.scrollingElement || e.target).scrollTop; 26 | 27 | if (!lastScroll) { 28 | lastScroll = Y; 29 | } 30 | 31 | if (Y >= this.height) { 32 | this.setState({ isFixed: true }); 33 | if (lastScroll <= Y) { 34 | // reset, is scrolling down 35 | firstReverse = 0; 36 | this.setState({ isShown: false }); 37 | } else { 38 | if ((firstReverse - Y) > this.buffer) { 39 | this.setState({ isShown: true }); 40 | } 41 | firstReverse = firstReverse || Y; 42 | } 43 | lastScroll = Y; 44 | } else { 45 | firstReverse = 0; 46 | this.setState({ isFixed: false, isShown: false }); 47 | } 48 | }; 49 | } 50 | 51 | componentDidMount() { 52 | this.height = this.base.offsetHeight; 53 | this.parent = this.parent || this.base.parentNode; 54 | this.props.disabled || toNode(this.parent).addEventListener('scroll', this.onScroll, { passive:true }); 55 | } 56 | 57 | componentWillReceiveProps(props) { 58 | const el = this.parent; 59 | const prev = this.props.disabled; 60 | // is newly enabled 61 | (prev && !props.disabled) && toNode(el).addEventListener('scroll', this.onScroll, { passive:true }); 62 | // is newly disabled 63 | (!prev && props.disabled) && toNode(el).removeEventListener('scroll', this.onScroll); 64 | } 65 | 66 | shouldComponentUpdate(props, state) { 67 | const now = this.state; 68 | return props !== this.props 69 | || state.isFixed !== now.isFixed 70 | || state.isReady !== now.isReady 71 | || state.isShown !== now.isShown; 72 | } 73 | 74 | componentDidUpdate(props, state) { 75 | const fix = this.state.isFixed; 76 | const now = this.state.isShown; 77 | // delay `isReady` application; transition flashing 78 | (state.isFixed !== fix) && setTimeout(() => this.setState({ isReady: fix }), 1); 79 | // call user callbacks if `shown` state changed 80 | if (state.isShown !== now) { 81 | ((now ? props.onShow : props.onHide) || noop).call(this, this.base); 82 | } 83 | } 84 | 85 | render(props, state) { 86 | let cls = props.className || ''; 87 | 88 | if (!props.disabled) { 89 | state.isFixed && (cls += ` ${props.fixClass || 'is--fixed'}`); 90 | state.isReady && (cls += ` ${props.readyClass || 'is--ready'}`); 91 | state.isShown && (cls += ` ${props.showClass || 'is--shown'}`); 92 | } 93 | 94 | return ( 95 |
96 |
{ props.children }
97 |
98 | ); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # preact-scroll-header [![NPM](https://img.shields.io/npm/v/preact-scroll-header.svg)](https://www.npmjs.com/package/preact-scroll-header) 2 | 3 | > A (800b gzip) header that will show/hide while scrolling; for :atom_symbol: [Preact](https://github.com/developit/preact) 4 | 5 | #### [Demo (standard)](https://jsfiddle.net/lukeed/cyfjsk50/) 6 | 7 | #### [Demo (inverse)](https://jsfiddle.net/lukeed/mxrse9k4/) 8 | 9 | ## Install 10 | 11 | ``` 12 | $ npm install --save preact-scroll-header 13 | ``` 14 | 15 | > :exclamation: **Pro Tip:** Use [Yarn](https://yarnpkg.com/) to install dependencies 3x faster than NPM! 16 | 17 | ```html 18 | 19 | ``` 20 | 21 | ## Usage 22 | 23 | Provide a `value`; everything else is optional. 24 | 25 | ```js 26 | import { h } from 'preact'; 27 | import ScrollHeader from 'preact-scroll-header'; 28 | 29 | console.log('SHOWN', el) } 33 | onHide={ el => console.log('HIDDEN', el) }> 34 |

Menu

35 |
36 | ``` 37 | 38 | ## Styles 39 | 40 | This component does not include any inline styles. Instead, classnames are assigned for every state the `` enters. This provides greater flexibility and control of your desired effects! 41 | 42 | However, there are some strong guidelines which you should not neglect. Below is an example for a simple slide-down effect: 43 | 44 | ```css 45 | /* start with "shown" position */ 46 | /* will-change, top, right, left early to avoid extra repaints */ 47 | .header { 48 | position: relative; 49 | will-change: transform; 50 | transform: translateY(0%); 51 | right: 0; 52 | left: 0; 53 | top: 0; 54 | } 55 | 56 | /* apply fixed; set start point */ 57 | .is--fixed { 58 | position: fixed; 59 | transform: translateY(-100%); 60 | } 61 | 62 | /* apply transition separately; no flicker */ 63 | .is--ready { 64 | transition: transform 0.2s ease-in-out; 65 | } 66 | 67 | /* set end point; with shadow */ 68 | .is--shown { 69 | transform: translateY(0%); 70 | box-shadow: 0 3px 6px rgba(0,0,0, 0.16); 71 | } 72 | ``` 73 | 74 | > **Note:** Assumes that "header" was added as your base `className`. All others are defaults. 75 | 76 | 77 | ## Properties 78 | 79 | #### id 80 | Type: `String`
81 | Default: `none` 82 | 83 | The `id` attribute to pass down. 84 | 85 | #### className 86 | Type: `String`
87 | Default: `none` 88 | 89 | The `className` attribute to pass down. Added to the wrapper element. 90 | 91 | #### fixClass 92 | Type: `String`
93 | Default: `'is--fixed'` 94 | 95 | The `className` to add when the header is out of view. This should apply a `position:fixed` style, as well as an initial `transform` value. 96 | 97 | #### readyClass 98 | Type: `String`
99 | Default: `'is--ready'` 100 | 101 | The `className` to add when the header has been "fixed". This should apply a `transition` value to your header, which should always be separated from your [`fixClass`](#fixClass). 102 | 103 | > **Note:** Applying a `transition` _before_ this class (via base style or `fixClass`) will cause the `` to flicker into view before hiding. 104 | 105 | #### showClass 106 | Type: `String`
107 | Default: `'is--shown'` 108 | 109 | The `className` to add when the header should be revealed. This should apply your desired `transform` effect. Class is only applied when the `` is out of view and has been "fixed". 110 | 111 | #### buffer 112 | Type: `Number`
113 | Default: `0` 114 | 115 | The number of pixels to scroll before applying your [`showClass`](#showClass). By default, the `` will be shown immediately after user scrolls up. 116 | 117 | #### listenTo 118 | Type: `String`
119 | Default: `this.base.parentNode` 120 | 121 | The "scroller" element that will fire `scroll` events. Works well with customized viewports, where `document.body` is not scrollable and/or controlling overflow. 122 | 123 | #### disabled 124 | Type: `Boolean`
125 | Default: `false` 126 | 127 | Whether or not to disable the show/hide behavior. If `true`, **will not** add `fixClass`, `readyClass`, or `showClass`. 128 | 129 | #### onShow 130 | Type: `Function` 131 | 132 | The callback function when the header is to be shown. Receivies the DOM element as its only argument, but is bound to the `ScrollHeader` component context. 133 | 134 | #### onHide 135 | Type: `Function` 136 | 137 | The callback function when the header is to be hidden. Receivies the DOM element as its only argument, but is bound to the `ScrollHeader` component context. 138 | 139 | 140 | ## License 141 | 142 | MIT © [Luke Edwards](https://lukeed.com) 143 | --------------------------------------------------------------------------------