├── .gitattributes ├── .gitignore ├── LICENSE ├── README.md ├── dist ├── base.98fd6c19.css ├── favicon.26242483.ico ├── index.html └── js.00a46daa.js ├── package.json └── src ├── css └── base.css ├── favicon.ico ├── index.html └── js ├── index.js └── infinitemenu.js /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /.cache 3 | package-lock.json -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2009 - 2020 [Codrops](https://tympanus.net/codrops) 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Scroll Loop Menu 2 | 3 | A simple infinitely scrollable menu based on the [demo](https://codepen.io/vincentorback/pen/OpdNJa) by Vincent Orback. 4 | 5 | ![Scroll Loop Menu](https://tympanus.net/codrops/wp-content/uploads/2020/05/InfiniteScrollMenu_featured.jpg) 6 | 7 | [Article on Codrops](https://tympanus.net/codrops/?p=49748) 8 | 9 | [Demo](http://tympanus.net/Tutorials/ScrollLoopMenu/) 10 | 11 | 12 | ## Installation 13 | 14 | Install dependencies: 15 | 16 | ``` 17 | npm install 18 | ``` 19 | 20 | Compile the code for development and start a local server: 21 | 22 | ``` 23 | npm start 24 | ``` 25 | 26 | Create the build: 27 | 28 | ``` 29 | npm run build 30 | ``` 31 | 32 | ## Misc 33 | 34 | Follow Codrops: [Twitter](http://www.twitter.com/codrops), [Facebook](http://www.facebook.com/codrops), [GitHub](https://github.com/codrops), [Instagram](https://www.instagram.com/codropsss/) 35 | 36 | ## License 37 | [MIT](LICENSE) 38 | 39 | Made with :blue_heart: by [Codrops](http://www.codrops.com) -------------------------------------------------------------------------------- /dist/base.98fd6c19.css: -------------------------------------------------------------------------------- 1 | *, 2 | *::after, 3 | *::before { 4 | box-sizing: border-box; 5 | } 6 | 7 | :root { 8 | font-size: 15px; 9 | } 10 | 11 | .js body:not(.mobile) { 12 | height: 100%; 13 | overflow: hidden; 14 | } 15 | 16 | body { 17 | margin: 0; 18 | --color-text: #111; 19 | --color-bg: #fff; 20 | --color-link: #c0092b; 21 | --color-link-hover: #000; 22 | --color-menu: #000; 23 | --color-menu-hover: #c0092b; 24 | color: var(--color-text); 25 | background-color: var(--color-bg); 26 | -webkit-font-smoothing: antialiased; 27 | -moz-osx-font-smoothing: grayscale; 28 | font-family: freight-big-pro, "Minion Pro", serif; 29 | font-weight: 300; 30 | } 31 | 32 | a { 33 | text-decoration: none; 34 | color: var(--color-link); 35 | outline: none; 36 | cursor: pointer; 37 | } 38 | 39 | a:hover, 40 | a:focus { 41 | color: var(--color-link-hover); 42 | outline: none; 43 | } 44 | 45 | .frame { 46 | top: 0; 47 | padding: 1rem; 48 | position: fixed; 49 | z-index: 1000; 50 | font-weight: 600; 51 | font-style: italic; 52 | background: #f0f0f0; 53 | width: 100%; 54 | display: flex; 55 | } 56 | 57 | .frame__title { 58 | font-size: 1rem; 59 | margin: 0; 60 | font-weight: 600; 61 | } 62 | 63 | .frame__links { 64 | margin: 0 1rem; 65 | } 66 | 67 | .frame__links a:not(:last-child) { 68 | margin-right: 1rem; 69 | } 70 | 71 | .frame__button { 72 | color: var(--color-link); 73 | margin-left: auto; 74 | } 75 | 76 | .menu { 77 | width: 100vw; 78 | height: 100vh; 79 | position: relative; 80 | overflow: auto; 81 | -webkit-overflow-scrolling: touch; 82 | scrollbar-width: none; /* Hide scrollbar in FF */ 83 | display: flex; 84 | flex-direction: column; 85 | align-items: flex-end; 86 | text-align: right; 87 | -webkit-touch-callout: none; 88 | -webkit-user-select: none; 89 | -moz-user-select: none; 90 | -ms-user-select: none; 91 | user-select: none; 92 | } 93 | 94 | .mobile .menu { 95 | padding: 5rem 0; 96 | height: auto; 97 | } 98 | 99 | .menu__item { 100 | flex: none; 101 | margin-right: 4rem; 102 | padding: 0 2rem 0 0; 103 | } 104 | 105 | .menu__item-inner { 106 | white-space: nowrap; 107 | position: relative; 108 | cursor: pointer; 109 | font-size: 7vw; 110 | padding: 0.5rem; 111 | display: block; 112 | color: var(--color-menu); 113 | transition: transform 0.2s; 114 | } 115 | 116 | .menu__item-inner:hover { 117 | font-style: italic; 118 | transform: translate3d(2rem,0,0); 119 | color: var(--color-menu-hover); 120 | } 121 | 122 | .menu__item-inner::before { 123 | content: ''; 124 | top: 55%; 125 | width: 3.5rem; 126 | height: 1px; 127 | background: currentColor; 128 | position: absolute; 129 | right: calc(100% + 2rem); 130 | opacity: 0; 131 | pointer-events: none; 132 | } 133 | 134 | .menu__item-inner:hover::before { 135 | opacity: 1; 136 | } 137 | 138 | /* Pseudo-element for making sure that hover area is active */ 139 | .menu__item-inner:hover::after { 140 | content: ''; 141 | position: absolute; 142 | top: 0; 143 | left: -5.5rem; 144 | right: 0; 145 | height: 100%; 146 | } 147 | 148 | ::-webkit-scrollbar { 149 | display: none; 150 | } 151 | 152 | @media screen and (min-width: 53em) { 153 | .frame { 154 | background: none; 155 | display: grid; 156 | grid-template-areas: 'title button' 157 | 'links ...'; 158 | padding: 3rem 4rem; 159 | pointer-events: none; 160 | } 161 | .frame__links { 162 | margin: 3rem 0 2rem; 163 | grid-area: links; 164 | justify-self: start; 165 | } 166 | .frame__links a { 167 | display: block; 168 | pointer-events: auto; 169 | } 170 | .frame__button { 171 | grid-area: button; 172 | justify-self: end; 173 | } 174 | .menu__item { 175 | margin-right: 25vw; 176 | } 177 | .menu__item-inner { 178 | padding: 1vh 0; 179 | font-size: 9.5vh; 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /dist/favicon.26242483.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codrops/ScrollLoopMenu/38257822c0bf1579ded7a17e6800acfd808014d3/dist/favicon.26242483.ico -------------------------------------------------------------------------------- /dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Scroll Loop Menu | Codrops 8 | 9 | 10 | 11 | 12 | 13 | 14 | 23 | 24 | 25 |
26 |

Scroll Loop Menu

27 | 31 | 32 |
33 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /dist/js.00a46daa.js: -------------------------------------------------------------------------------- 1 | // modules are defined as an array 2 | // [ module function, map of requires ] 3 | // 4 | // map of requires is short require name -> numeric require 5 | // 6 | // anything defined in a previous bundle is accessed via the 7 | // orig method which is the require for previous bundles 8 | parcelRequire = (function (modules, cache, entry, globalName) { 9 | // Save the require from previous bundle to this closure if any 10 | var previousRequire = typeof parcelRequire === 'function' && parcelRequire; 11 | var nodeRequire = typeof require === 'function' && require; 12 | 13 | function newRequire(name, jumped) { 14 | if (!cache[name]) { 15 | if (!modules[name]) { 16 | // if we cannot find the module within our internal map or 17 | // cache jump to the current global require ie. the last bundle 18 | // that was added to the page. 19 | var currentRequire = typeof parcelRequire === 'function' && parcelRequire; 20 | if (!jumped && currentRequire) { 21 | return currentRequire(name, true); 22 | } 23 | 24 | // If there are other bundles on this page the require from the 25 | // previous one is saved to 'previousRequire'. Repeat this as 26 | // many times as there are bundles until the module is found or 27 | // we exhaust the require chain. 28 | if (previousRequire) { 29 | return previousRequire(name, true); 30 | } 31 | 32 | // Try the node require function if it exists. 33 | if (nodeRequire && typeof name === 'string') { 34 | return nodeRequire(name); 35 | } 36 | 37 | var err = new Error('Cannot find module \'' + name + '\''); 38 | err.code = 'MODULE_NOT_FOUND'; 39 | throw err; 40 | } 41 | 42 | localRequire.resolve = resolve; 43 | localRequire.cache = {}; 44 | 45 | var module = cache[name] = new newRequire.Module(name); 46 | 47 | modules[name][0].call(module.exports, localRequire, module, module.exports, this); 48 | } 49 | 50 | return cache[name].exports; 51 | 52 | function localRequire(x){ 53 | return newRequire(localRequire.resolve(x)); 54 | } 55 | 56 | function resolve(x){ 57 | return modules[name][1][x] || x; 58 | } 59 | } 60 | 61 | function Module(moduleName) { 62 | this.id = moduleName; 63 | this.bundle = newRequire; 64 | this.exports = {}; 65 | } 66 | 67 | newRequire.isParcelRequire = true; 68 | newRequire.Module = Module; 69 | newRequire.modules = modules; 70 | newRequire.cache = cache; 71 | newRequire.parent = previousRequire; 72 | newRequire.register = function (id, exports) { 73 | modules[id] = [function (require, module) { 74 | module.exports = exports; 75 | }, {}]; 76 | }; 77 | 78 | var error; 79 | for (var i = 0; i < entry.length; i++) { 80 | try { 81 | newRequire(entry[i]); 82 | } catch (e) { 83 | // Save first error but execute all entries 84 | if (!error) { 85 | error = e; 86 | } 87 | } 88 | } 89 | 90 | if (entry.length) { 91 | // Expose entry point to Node, AMD or browser globals 92 | // Based on https://github.com/ForbesLindesay/umd/blob/master/template.js 93 | var mainExports = newRequire(entry[entry.length - 1]); 94 | 95 | // CommonJS 96 | if (typeof exports === "object" && typeof module !== "undefined") { 97 | module.exports = mainExports; 98 | 99 | // RequireJS 100 | } else if (typeof define === "function" && define.amd) { 101 | define(function () { 102 | return mainExports; 103 | }); 104 | 105 | // 19 | 20 | 21 |
22 |

Scroll Loop Menu

23 | 27 | 28 |
29 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /src/js/index.js: -------------------------------------------------------------------------------- 1 | import InfiniteMenu from './infinitemenu'; 2 | 3 | const menu = new InfiniteMenu(document.querySelector('nav.menu')); -------------------------------------------------------------------------------- /src/js/infinitemenu.js: -------------------------------------------------------------------------------- 1 | const winsize = {width: window.innerWidth, height: window.innerHeight}; 2 | 3 | // https://stackoverflow.com/a/3540295 4 | let isMobile = false; //initiate as false 5 | // device detection 6 | if(/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|ipad|iris|kindle|Android|Silk|lge |maemo|midp|mmp|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows (ce|phone)|xda|xiino/i.test(navigator.userAgent) 7 | || /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(navigator.userAgent.substr(0,4))) { 8 | isMobile = true; 9 | } 10 | 11 | export default class InfiniteMenu { 12 | constructor(el) { 13 | if ( !isMobile ) { 14 | this.DOM = {el: el}; 15 | this.DOM.menuItems = [...this.DOM.el.querySelectorAll('.menu__item')]; 16 | 17 | this.cloneItems(); 18 | this.initScroll(); 19 | 20 | this.initEvents(); 21 | 22 | // rAF/loop 23 | requestAnimationFrame(() => this.render()); 24 | } 25 | else { 26 | document.body.classList.add('mobile'); 27 | } 28 | } 29 | getScrollPos() { 30 | return (this.DOM.el.pageYOffset || this.DOM.el.scrollTop) - (this.DOM.el.clientTop || 0); 31 | } 32 | setScrollPos(pos) { 33 | this.DOM.el.scrollTop = pos; 34 | } 35 | // Create menu items clones and append them to the menu items list 36 | // total clones = number of menu items that fit in the viewport 37 | cloneItems() { 38 | // Get the height of one menu item 39 | const itemHeight = this.DOM.menuItems[0].offsetHeight; 40 | // How many items fit in the window? 41 | const fitIn = Math.ceil(winsize.height / itemHeight); 42 | // Create [fitIn] clones from the beginning of the list 43 | 44 | // Remove any 45 | this.DOM.el.querySelectorAll('.loop__clone').forEach(clone => this.DOM.el.removeChild(clone)); 46 | // Add clones 47 | let totalClones = 0; 48 | this.DOM.menuItems.filter((_, index) => (index < fitIn)).map(target => { 49 | const clone = target.cloneNode(true); 50 | clone.classList.add('loop__clone'); 51 | this.DOM.el.appendChild(clone); 52 | ++totalClones; 53 | }); 54 | 55 | // All clones height 56 | this.clonesHeight = totalClones * itemHeight; 57 | // Scrollable area height 58 | this.scrollHeight = this.DOM.el.scrollHeight; 59 | } 60 | initEvents() { 61 | window.addEventListener('resize', () => this.resize()); 62 | } 63 | resize() { 64 | this.cloneItems(); 65 | this.initScroll(); 66 | } 67 | initScroll() { 68 | // Scroll 1 pixel to allow upwards scrolling 69 | this.scrollPos = this.getScrollPos(); 70 | if (this.scrollPos <= 0) { 71 | this.setScrollPos(1); 72 | } 73 | } 74 | scrollUpdate() { 75 | this.scrollPos = this.getScrollPos(); 76 | 77 | if ( this.clonesHeight + this.scrollPos >= this.scrollHeight ) { 78 | // Scroll to the top when you’ve reached the bottom 79 | this.setScrollPos(1); // Scroll down 1 pixel to allow upwards scrolling 80 | } 81 | else if ( this.scrollPos <= 0 ) { 82 | // Scroll to the bottom when you reach the top 83 | this.setScrollPos(this.scrollHeight - this.clonesHeight); 84 | } 85 | } 86 | render() { 87 | this.scrollUpdate(); 88 | requestAnimationFrame(() => this.render()); 89 | } 90 | } --------------------------------------------------------------------------------