├── .gitignore ├── .npmignore ├── .nvmrc ├── .vscode └── settings.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── cypress.config.ts ├── cypress ├── e2e │ ├── scroll-element.cy.js │ └── scroll-window.cy.js └── support │ └── e2e.js ├── docs ├── css │ └── style.css ├── docs.js ├── docs.js.map ├── docs.ts ├── img │ ├── isolines.svg │ └── star.svg ├── index.html └── test.html ├── package-lock.json ├── package.json ├── src └── index.ts ├── test.js ├── tsconfig-base.json ├── tsconfig-cjs.json ├── tsconfig-demo.json └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | config.codekit* 2 | .codekit-cache 3 | node_modules 4 | npm-debug.log 5 | .idea/ 6 | .DS_Store 7 | .cache 8 | .parcel-cache 9 | dist 10 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | config.codekit* 2 | .codekit-cache 3 | node_modules 4 | npm-debug.log 5 | .idea/ 6 | .DS_Store 7 | .cache 8 | cypress 9 | docs 10 | dist 11 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v20.18.1 2 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "cSpell.words": [] 4 | } 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ### v2.3.2 4 | 5 | 17.01.2025. 6 | 7 | **Added** 8 | 9 | - Improved the readme. Added two sections - on scrolling iframes and CSS properties that can break the animation. 10 | 11 | --- 12 | 13 | ### v2.3.1 14 | 15 | 16.01.2025. 16 | 17 | **Added** 18 | 19 | - Added a warning that `scroll-snap-type: x mandatory` might break the animation [#71](https://github.com/Stanko/animated-scroll-to/pull/71) 20 | 21 | --- 22 | 23 | ### v2.3.0 24 | 25 | 23.06.2022. 26 | 27 | **Changed** 28 | 29 | - Switched to [keen](https://github.com/Stanko/keen/) as build system. 30 | - Added ESM and CJS modules. 31 | - Upgraded dev dependencies. 32 | 33 | --- 34 | 35 | ### v2.2.0 36 | 37 | 18.03.2020. 38 | 39 | **Fixed** 40 | 41 | - Added a warning if the scrolling element has `scroll-behavior: smooth` [#55](https://github.com/Stanko/animated-scroll-to/pull/55) 42 | 43 | --- 44 | 45 | ### v2.1.0 46 | 47 | 08.03.2020. 48 | 49 | **Removed** 50 | 51 | - Removed support for using the script in browsers directly. It was broken, so probably was not using it anyway. Use version 1 if you really need it. 52 | 53 | --- 54 | 55 | ### v2.0.12 56 | 57 | 27.10.2020. 58 | 59 | **Fixed** 60 | 61 | - Reverted labeled tuple coordinates for TCoords [#49](https://github.com/Stanko/animated-scroll-to/pull/49) as it breaks on older TS version [#50](https://github.com/Stanko/animated-scroll-to/issues/50). 62 | 63 | --- 64 | 65 | ### v2.0.11 66 | 67 | 19.10.2020. 68 | 69 | **Fixed** 70 | 71 | - Improved types, labeled tuple coordinates for TCoords [#49](https://github.com/Stanko/animated-scroll-to/pull/49). 72 | - Fixed build on windows [#49](https://github.com/Stanko/animated-scroll-to/pull/49). 73 | 74 | --- 75 | 76 | ### v2.0.9, 2.0.10 77 | 78 | 25.06.2020. 79 | 80 | **Fixed** 81 | 82 | - Added `Promise` as a return type in the types definition file. 83 | 84 | --- 85 | 86 | ### v2.0.8 87 | 88 | 07.05.2020. 89 | 90 | **Fixed** 91 | 92 | - Event options were missing in `removeEventListener` [#44](https://github.com/Stanko/animated-scroll-to/pull/44) 93 | 94 | --- 95 | 96 | ### v2.0.6 and v2.0.7 97 | 98 | 20.04.2020. 99 | 100 | **Fixed** 101 | 102 | - Calculating element offset inside of element was sometimes a pixel off. 103 | - Active animations weren't cleared on animation end 104 | - Now error is thrown in "elementToScroll" is not a parent of "scrollToElement" 105 | 106 | --- 107 | 108 | ### v2.0.4 and v2.0.5 109 | 110 | 09.11.2019. 111 | 112 | **Fixed** 113 | 114 | - Fixed TS types [#36](https://github.com/Stanko/animated-scroll-to/issues/36) 115 | 116 | --- 117 | 118 | ### v2.0.3 119 | 120 | 03.10.2019. 121 | 122 | **Fixed** 123 | 124 | - Fixed library breaking when running on server [#34](https://github.com/Stanko/animated-scroll-to/issues/34) 125 | 126 | --- 127 | 128 | ### v2.0.2 129 | 130 | 26.09.2019. 131 | 132 | **Changed** 133 | 134 | - Switched to commonjs module [#33](https://github.com/Stanko/animated-scroll-to/issues/33) 135 | 136 | --- 137 | 138 | ### v2.0.0 and v2.0.1 139 | 140 | 23.09.2019. 141 | 142 | **Changed** 143 | 144 | - Full TypeScript rewrite 145 | - New method signatures 146 | - Method now returns a promise 147 | 148 | --- 149 | 150 | ### v1.3.1 151 | 152 | 14.09.2019. 153 | 154 | **Fixed** 155 | 156 | - Fixed if element was scrolling and cancelOnUserAction was passed the whole page couldn't scroll [#28](https://github.com/Stanko/animated-scroll-to/issues/28) 157 | 158 | --- 159 | 160 | ### v1.3.0 161 | 162 | 02.09.2019. 163 | 164 | **Changed** 165 | 166 | - `onComplete` callback now has a boolean argument `isCanceledByUserAction`, and it is called even when scroll is canceled [#26](https://github.com/Stanko/animated-scroll-to/issues/26) 167 | 168 | --- 169 | 170 | ### v1.2.2 171 | 172 | 07.06.2018. 173 | 174 | **Fixed** 175 | 176 | - Fix event listeners not being removed in IE11 [#19](https://github.com/Stanko/animated-scroll-to/pull/19) 177 | - Updated `index.d.ts` to add `offset` [#19](https://github.com/Stanko/animated-scroll-to/pull/19) 178 | 179 | --- 180 | 181 | ### v1.2.1 182 | 183 | 17.05.2018. 184 | 185 | **Added** 186 | 187 | - Added `offset` option, kudos to @weotch [#17](https://github.com/Stanko/animated-scroll-to/pull/17) 188 | 189 | **Fixed** 190 | 191 | - Chrome would throw `Unable to preventDefault inside passive event listener invocation.` when `cancelOnUserAction` was set to `false` [#18](https://github.com/Stanko/animated-scroll-to/issues/18) 192 | 193 | --- 194 | 195 | ### v1.2.0 196 | 197 | 22.04.2018. 198 | 199 | **Added** 200 | 201 | - Added `horizontal` option, kudos to @jesseditson [#15](https://github.com/Stanko/animated-scroll-to/pull/15) 202 | - Changelog 203 | 204 | --- 205 | 206 | ### v1.1.11 207 | 208 | 09.04.2018. 209 | 210 | **Added** 211 | 212 | - Set events to be `passive` by default, kudos to @cybot1711 [#14](https://github.com/Stanko/animated-scroll-to/pull/14) 213 | 214 | --- 215 | 216 | For changes prior version 1.1.11 please check the [commit list](https://github.com/Stanko/animated-scroll-to/commits/master). 217 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright 2016-present Stanko Tadić 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # animated-scroll-to 2 | 3 | [![npm version](https://img.shields.io/npm/v/animated-scroll-to.svg?style=flat-square)](https://www.npmjs.com/package/animated-scroll-to) 4 | [![npm downloads](https://img.shields.io/npm/dm/animated-scroll-to.svg?style=flat-square)](https://www.npmjs.com/package/animated-scroll-to) 5 | 6 | Lightweight (1.9kb gzipped) scroll to function with a powerful API. Scrolls window or any other DOM element. 7 | 8 | The main difference to other libraries is that it accepts speed of scrolling instead of duration. This way scrolling for 200 pixels will last less than scrolling 10000 pixels. Minimum and maximum duration are configurable and set to reasonable defaults (250 and 3000ms). 9 | 10 | All changes are tracked in [CHANGELOG](CHANGELOG.md). 11 | 12 | ## Demo 13 | 14 | Play with the [live demo.](https://stanko.github.io/animated-scroll-to/) 15 | 16 | ## Features 17 | 18 | - Accepts speed per 1000px instead of duration 19 | - Scrolls window or any other DOM element horizontally and vertically 20 | - Returns a promise with a boolean flag which tells you if desired scroll position was reached (for IE you'll need to include a `Promise` [polyfill](https://github.com/stefanpenner/es6-promise)) 21 | - If called multiple times on the same element, it will cancel prior animations 22 | - Optionally prevent user from scrolling until scrolling animation is finished 23 | 24 | ## Usage 25 | 26 | Grab it from npm 27 | 28 | ``` 29 | npm install animated-scroll-to 30 | ``` 31 | 32 | and import it in your app 33 | 34 | ```javascript 35 | import animateScrollTo from 'animated-scroll-to'; 36 | 37 | // It returns a promise which will be resolved when scroll animation is finished 38 | 39 | animateScrollTo(500).then((hasScrolledToPosition) => { 40 | // scroll animation is finished 41 | 42 | // "hasScrolledToPosition" indicates if page/element 43 | // was scrolled to a desired position 44 | // or if animation got interrupted 45 | if (hasScrolledToPosition) { 46 | // page is scrolled to a desired position 47 | } else { 48 | // scroll animation was interrupted by user 49 | // or by another call of "animateScrollTo" 50 | } 51 | }); 52 | ``` 53 | 54 | ### Method signatures 55 | 56 | Library has three ways to call it: 57 | 58 | ```js 59 | // "y" is a desired vertical scroll position to scroll to 60 | function animateScrollTo(y, options); 61 | 62 | // "coords" are an array "[x, y]" 63 | // Where "x" and "y" are desired horizontal and vertical positions to scroll to 64 | // Both "x" and "y" can be null 65 | // which will result in keeping the current scroll position for that axis 66 | function animateScrollTo(coords, options); 67 | 68 | // If you pass a DOM element, page will be scrolled to it 69 | function animateScrollTo(scrollToElement, options); 70 | ``` 71 | 72 | Example usage of each method: 73 | 74 | ```js 75 | // Scrolls page vertically to 1000px 76 | animateScrollTo(1000); 77 | 78 | // Scrolls page horizontally to 1000px but keeps vertical scroll position 79 | animateScrollTo([1000, null]); 80 | 81 | // Scrolls page horizontally too 1000px and vertically to 500px 82 | animateScrollTo([1000, 500]); 83 | 84 | // Scrolls page both horizontally and vertically to ".my-element" 85 | animateScrollTo(document.querySelector('.my-element')); 86 | ``` 87 | 88 | ### Options 89 | 90 | Options with their default values: 91 | 92 | ```js 93 | const defaultOptions = { 94 | // Indicated if scroll animation should be canceled on user action (scroll/keypress/touch) 95 | // if set to "false" user input will be disabled until scroll animation is complete 96 | cancelOnUserAction: true, 97 | 98 | // Animation easing function, with "easeOutCubic" as default 99 | easing: (t) => --t * t * t + 1, 100 | 101 | // DOM element that should be scrolled 102 | // Example: document.querySelector('#element-to-scroll'), 103 | elementToScroll: window, 104 | 105 | // Horizontal scroll offset 106 | // Practical when you are scrolling to a DOM element and want to add some padding 107 | horizontalOffset: 0, 108 | 109 | // Maximum duration of the scroll animation 110 | maxDuration: 3000, 111 | 112 | // Minimum duration of the scroll animation 113 | minDuration: 250, 114 | 115 | // Duration of the scroll per 1000px 116 | speed: 500, 117 | 118 | // Vertical scroll offset 119 | // Practical when you are scrolling to a DOM element and want to add some padding 120 | verticalOffset: 0, 121 | }; 122 | ``` 123 | 124 | ### Easing 125 | 126 | By default library is using `easeOutCubic` easing function. You can pass a custom function only considering the `t` value for the range `[0, 1] => [0, 1]`. 127 | 128 | To make things easier I provided a list of common easing function below: 129 | 130 | ```js 131 | /* 132 | * Easing Functions 133 | * https://gist.github.com/gre/1650294 134 | */ 135 | const EasingFunctions = { 136 | // no easing, no acceleration 137 | linear: (t) => { 138 | return t; 139 | }, 140 | // accelerating from zero velocity 141 | easeInQuad: (t) => { 142 | return t * t; 143 | }, 144 | // decelerating to zero velocity 145 | easeOutQuad: (t) => { 146 | return t * (2 - t); 147 | }, 148 | // acceleration until halfway, then deceleration 149 | easeInOutQuad: (t) => { 150 | return t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t; 151 | }, 152 | // accelerating from zero velocity 153 | easeInCubic: (t) => { 154 | return t * t * t; 155 | }, 156 | // decelerating to zero velocity 157 | easeOutCubic: (t) => { 158 | return --t * t * t + 1; 159 | }, 160 | // acceleration until halfway, then deceleration 161 | easeInOutCubic: (t) => { 162 | return t < 0.5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1; 163 | }, 164 | // accelerating from zero velocity 165 | easeInQuart: (t) => { 166 | return t * t * t * t; 167 | }, 168 | // decelerating to zero velocity 169 | easeOutQuart: (t) => { 170 | return 1 - --t * t * t * t; 171 | }, 172 | // acceleration until halfway, then deceleration 173 | easeInOutQuart: (t) => { 174 | return t < 0.5 ? 8 * t * t * t * t : 1 - 8 * --t * t * t * t; 175 | }, 176 | // accelerating from zero velocity 177 | easeInQuint: (t) => { 178 | return t * t * t * t * t; 179 | }, 180 | // decelerating to zero velocity 181 | easeOutQuint: (t) => { 182 | return 1 + --t * t * t * t * t; 183 | }, 184 | // acceleration until halfway, then deceleration 185 | easeInOutQuint: (t) => { 186 | return t < 0.5 ? 16 * t * t * t * t * t : 1 + 16 * --t * t * t * t * t; 187 | }, 188 | }; 189 | ``` 190 | 191 | ## Certain CSS properties might break the animation 192 | 193 | As library is using an animation loop to scroll, some CSS properties might clash with the approach and break the animation. 194 | 195 | The library will warn you about the ones that are know to break the animation: 196 | 197 | - `scroll-behavior: smooth` 198 | - `scroll-snap-type: x mandatory` (or `y mandatory` depending on the axis you scroll) 199 | 200 | ## Scrolling an iframe 201 | 202 | You can also use the library to scroll iframes from the same domain (check [MDN contentWindow documentation](https://developer.mozilla.org/en-US/docs/Web/API/HTMLIFrameElement/contentWindow)). 203 | 204 | ```js 205 | const iframeWindow = 206 | document.querySelector('#my-iframe').contentWindow.document.documentElement; 207 | 208 | animateScrollTo(500, { 209 | elementToScroll: iframeWindow, 210 | }); 211 | ``` 212 | 213 | **Please note:** If the iframe is not on the same domain as the base page, you are going to get a cross origin error. 214 | 215 | ## Why? 216 | 217 | I wasn't able to find standalone, simple and working solution. 218 | 219 | ## Browser support 220 | 221 | Anything that supports `requestAnimationFrame` and `Promise`. For Internet Explorer you'll need to add [es6-promise polyfill](https://github.com/stefanpenner/es6-promise). 222 | 223 | For IE9 and lower, you'll to provide [requestAnimationFrame polyfill](https://gist.github.com/paulirish/1579671). 224 | 225 | For IE8 and lower, you'll need to polyfill `Array.forEach` as well. Haven't tested this though. 226 | 227 | ## It is missing <insert a feature here> 228 | 229 | I really tried to keep simple and lightweight. 230 | If you are missing something, feel free to open a pull request. 231 | 232 | ## Version 1 233 | 234 | I advise you to use v2, as v1 is deprecated. But it is still available on [v1 branch](https://github.com/Stanko/animated-scroll-to/tree/v1) and on [npm](https://www.npmjs.com/package/animated-scroll-to/v/1.3.1). 235 | -------------------------------------------------------------------------------- /cypress.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'cypress'; 2 | 3 | export default defineConfig({ 4 | video: false, 5 | screenshotOnRunFailure: false, 6 | e2e: {}, 7 | }); 8 | -------------------------------------------------------------------------------- /cypress/e2e/scroll-element.cy.js: -------------------------------------------------------------------------------- 1 | describe('Scrolling the element', function () { 2 | Cypress.on('window:before:load', (win) => { 3 | cy.spy(win.console, 'warn'); 4 | }); 5 | 6 | it('scrolls the element vertically', function () { 7 | cy.visit('http://localhost:8000/test.html'); 8 | 9 | cy.window().then((win) => { 10 | cy.get('.element-to-scroll').then((elementToScroll) => { 11 | return win 12 | .animateScrollTo(1000, { 13 | elementToScroll: elementToScroll[0], 14 | }) 15 | .then(() => { 16 | cy.expect(elementToScroll[0].scrollTop).to.equal(1000); 17 | }); 18 | }, 1000); 19 | }, 1000); 20 | }); 21 | 22 | it('scrolls the element horizontally and vertically', function () { 23 | cy.visit('http://localhost:8000/test.html'); 24 | 25 | cy.window().then((win) => { 26 | cy.get('.element-to-scroll').then((elementToScroll) => { 27 | return win 28 | .animateScrollTo([500, 500], { 29 | elementToScroll: elementToScroll[0], 30 | }) 31 | .then(() => { 32 | cy.expect(elementToScroll[0].scrollTop).to.equal(500); 33 | cy.expect(elementToScroll[0].scrollLeft).to.equal(500); 34 | }); 35 | }, 1000); 36 | }, 1000); 37 | }); 38 | 39 | it('scrolls the element horizontally only', function () { 40 | cy.visit('http://localhost:8000/test.html'); 41 | 42 | cy.window().then((win) => { 43 | cy.get('.element-to-scroll').then((elementToScroll) => { 44 | const currentY = elementToScroll[0].scrollTop; 45 | 46 | return win 47 | .animateScrollTo([500, null], { 48 | elementToScroll: elementToScroll[0], 49 | }) 50 | .then(() => { 51 | cy.expect(elementToScroll[0].scrollLeft).to.equal(500); 52 | cy.expect(elementToScroll[0].scrollTop).to.equal(currentY); 53 | }); 54 | }, 1000); 55 | }, 1000); 56 | }); 57 | 58 | it('scrolls the element vertically only', function () { 59 | cy.visit('http://localhost:8000/test.html'); 60 | 61 | cy.window().then((win) => { 62 | cy.get('.element-to-scroll').then((elementToScroll) => { 63 | const currentX = elementToScroll[0].scrollLeft; 64 | 65 | return win 66 | .animateScrollTo([null, 1000], { 67 | elementToScroll: elementToScroll[0], 68 | }) 69 | .then(() => { 70 | cy.expect(elementToScroll[0].scrollLeft).to.equal(currentX); 71 | cy.expect(elementToScroll[0].scrollTop).to.equal(1000); 72 | }); 73 | }, 1000); 74 | }, 1000); 75 | }); 76 | 77 | it('scrolls the element to element', function () { 78 | cy.visit('http://localhost:8000/test.html'); 79 | 80 | cy.window().then((win) => { 81 | cy.get('.element-to-scroll').then((elementToScroll) => { 82 | cy.get('.element-scroll-to').then((elementToScrollTo) => { 83 | return win 84 | .animateScrollTo(elementToScrollTo[0], { 85 | elementToScroll: elementToScroll[0], 86 | }) 87 | .then(() => { 88 | // Margins are hard coded in CSS 89 | cy.expect(elementToScroll[0].scrollLeft).to.equal(1000); 90 | cy.expect(elementToScroll[0].scrollTop).to.equal(1000); 91 | }); 92 | }, 1000); 93 | }, 1000); 94 | }, 1000); 95 | }); 96 | 97 | it('scrolls the element horizontally and vertically with offset', function () { 98 | cy.visit('http://localhost:8000/test.html'); 99 | 100 | cy.window().then((win) => { 101 | cy.get('.element-to-scroll').then((elementToScroll) => { 102 | return win 103 | .animateScrollTo([500, 500], { 104 | verticalOffset: 100, 105 | horizontalOffset: 100, 106 | elementToScroll: elementToScroll[0], 107 | }) 108 | .then(() => { 109 | cy.expect(elementToScroll[0].scrollTop).to.equal(600); 110 | cy.expect(elementToScroll[0].scrollLeft).to.equal(600); 111 | }); 112 | }, 1000); 113 | }, 1000); 114 | }); 115 | 116 | it('checks if console.warn is called when scroll-behavior: smooth is set', function () { 117 | cy.visit('http://localhost:8000/test.html'); 118 | 119 | cy.window().then((win) => { 120 | cy.get('.element-to-scroll').then((elementToScroll) => { 121 | elementToScroll[0].style.scrollBehavior = 'smooth'; 122 | 123 | return win 124 | .animateScrollTo(1000, { 125 | elementToScroll: elementToScroll[0], 126 | }) 127 | .then(() => { 128 | cy.expect(win.console.warn).to.have.callCount(1); 129 | }); 130 | }, 1000); 131 | }, 1000); 132 | }); 133 | 134 | it('checks if console.warn is called when scroll-snap-type: [x/y] mandatory is set', function () { 135 | cy.visit('http://localhost:8000/test.html'); 136 | 137 | cy.window().then((win) => { 138 | cy.get('.element-to-scroll').then((elementToScroll) => { 139 | elementToScroll[0].style.scrollSnapType = 'x mandatory'; 140 | 141 | return win 142 | .animateScrollTo(1000, { 143 | elementToScroll: elementToScroll[0], 144 | }) 145 | .then(() => { 146 | cy.expect(win.console.warn).to.have.callCount(1); 147 | }); 148 | }, 1000); 149 | }, 1000); 150 | }); 151 | }); 152 | -------------------------------------------------------------------------------- /cypress/e2e/scroll-window.cy.js: -------------------------------------------------------------------------------- 1 | describe('Scrolling the window', function () { 2 | Cypress.on('window:before:load', (win) => { 3 | cy.spy(win.console, 'warn'); 4 | }); 5 | 6 | it('scrolls the window vertically', function () { 7 | cy.visit('http://localhost:8000/test.html'); 8 | 9 | cy.window().then((win) => { 10 | return win.animateScrollTo(1000).then(() => { 11 | cy.expect(win.scrollY).to.equal(1000); 12 | }); 13 | }, 1000); 14 | }); 15 | 16 | it('scrolls the window horizontally and vertically', function () { 17 | cy.visit('http://localhost:8000/test.html'); 18 | 19 | cy.window().then((win) => { 20 | return win.animateScrollTo([500, 500]).then(() => { 21 | cy.expect(win.scrollY).to.equal(500); 22 | cy.expect(win.scrollX).to.equal(500); 23 | }); 24 | }, 1000); 25 | }); 26 | 27 | it('scrolls the window horizontally only', function () { 28 | cy.visit('http://localhost:8000/test.html'); 29 | 30 | cy.window().then((win) => { 31 | const currentY = win.scrollY; 32 | 33 | return win.animateScrollTo([500, null]).then(() => { 34 | cy.expect(win.scrollX).to.equal(500); 35 | cy.expect(win.scrollY).to.equal(currentY); 36 | }); 37 | }, 1000); 38 | }); 39 | 40 | it('scrolls the window vertically only', function () { 41 | cy.visit('http://localhost:8000/test.html'); 42 | 43 | cy.window().then((win) => { 44 | const currentX = win.scrollX; 45 | 46 | return win.animateScrollTo([null, 1000]).then(() => { 47 | cy.expect(win.scrollX).to.equal(currentX); 48 | cy.expect(win.scrollY).to.equal(1000); 49 | }); 50 | }, 1000); 51 | }); 52 | 53 | it('scrolls the window to element', function () { 54 | cy.visit('http://localhost:8000/test.html'); 55 | 56 | cy.window().then((win) => { 57 | cy.get('.window-scroll-to').then((elementToScrollTo) => { 58 | return win.animateScrollTo(elementToScrollTo[0]).then(() => { 59 | // Margins are hard coded in CSS 60 | cy.expect(win.scrollX).to.equal(1000); 61 | cy.expect(win.scrollY).to.equal(1000); 62 | }); 63 | }, 1000); 64 | }, 1000); 65 | }); 66 | 67 | it('scrolls the window horizontally and vertically with offset', function () { 68 | cy.visit('http://localhost:8000/test.html'); 69 | 70 | cy.window().then((win) => { 71 | return win 72 | .animateScrollTo([500, 500], { 73 | verticalOffset: 100, 74 | horizontalOffset: 100, 75 | }) 76 | .then(() => { 77 | cy.expect(win.scrollY).to.equal(600); 78 | cy.expect(win.scrollX).to.equal(600); 79 | }); 80 | }, 1000); 81 | }); 82 | 83 | it('animation finishes in correct duration when min and max durations are the same', function () { 84 | cy.visit('http://localhost:8000/test.html'); 85 | 86 | cy.window().then((win) => { 87 | const start = Date.now(); 88 | const THRESHOLD = 1000; 89 | const DURATION = 500; 90 | 91 | return win 92 | .animateScrollTo([500, 500], { 93 | minDuration: DURATION, 94 | maxDuration: DURATION, 95 | // Using linear easing to be sure timings are correct 96 | easing: (t) => t, 97 | }) 98 | .then(() => { 99 | const timePassed = Date.now() - start; 100 | cy.expect(Math.abs(timePassed - DURATION)).to.be.lessThan(THRESHOLD); 101 | }); 102 | }, 1000); 103 | }); 104 | 105 | it('animation finishes in correct duration when speed is set', function () { 106 | cy.visit('http://localhost:8000/test.html'); 107 | 108 | cy.window().then((win) => { 109 | const start = Date.now(); 110 | const THRESHOLD = 1000; 111 | const DURATION = 1000; // 1000px at 1000px per second = 1000ms 112 | 113 | win.scrollTo(0, 0); 114 | 115 | return win 116 | .animateScrollTo(1000, { 117 | minDuration: 0, 118 | maxDuration: 99999, 119 | speed: 1000, 120 | // Using linear easing to be sure timings are correct 121 | easing: (t) => t, 122 | }) 123 | .then(() => { 124 | const timePassed = Date.now() - start; 125 | cy.expect(Math.abs(timePassed - DURATION)).to.be.lessThan(THRESHOLD); 126 | }); 127 | }, 1000); 128 | }); 129 | 130 | it('checks if console.warn is called when scroll-behavior: smooth is set', function () { 131 | cy.visit('http://localhost:8000/test.html'); 132 | 133 | cy.document().then((doc) => { 134 | doc.documentElement.style.scrollBehavior = 'smooth'; 135 | 136 | cy.window().then((win) => { 137 | cy.get('.element-to-scroll').then((elementToScroll) => { 138 | return win.animateScrollTo(1000).then(() => { 139 | cy.expect(win.console.warn).to.have.callCount(1); 140 | }); 141 | }, 1000); 142 | }, 1000); 143 | }, 1000); 144 | }); 145 | 146 | it('checks if console.warn is called when scroll-snap-type: [x/y] mandatory is set', function () { 147 | cy.visit('http://localhost:8000/test.html'); 148 | 149 | cy.document().then((doc) => { 150 | doc.documentElement.style.scrollSnapType = 'x mandatory'; 151 | 152 | cy.window().then((win) => { 153 | cy.get('.element-to-scroll').then((elementToScroll) => { 154 | return win.animateScrollTo(1000).then(() => { 155 | cy.expect(win.console.warn).to.have.callCount(1); 156 | }); 157 | }, 1000); 158 | }, 1000); 159 | }, 1000); 160 | }); 161 | }); 162 | -------------------------------------------------------------------------------- /cypress/support/e2e.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/index.js is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | 18 | // Alternatively you can use CommonJS syntax: 19 | // require('./commands') 20 | -------------------------------------------------------------------------------- /docs/css/style.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --blue: hsl(233, 60%, 84%); 3 | --purple: hsl(260, 60%, 87%); 4 | --red: hsl(300, 60%, 90%); 5 | --yellow: hsl(46, 80%, 87%); 6 | --green: hsl(140, 50%, 87%); 7 | 8 | --thumb: rgba(0, 0, 0, 0.3); 9 | 10 | --text: #222428; 11 | --black: #121316; 12 | 13 | --accent: #3c00d3; 14 | 15 | --max-width: 650px; 16 | } 17 | 18 | /* ----- Global ----- */ 19 | 20 | html { 21 | font-size: 16px; 22 | } 23 | 24 | body { 25 | margin: 0; 26 | padding: 0; 27 | font-family: ui-rounded, 'Hiragino Maru Gothic ProN', Quicksand, Comfortaa, 28 | Manjari, 'Arial Rounded MT', 'Arial Rounded MT Bold', Calibri, 29 | source-sans-pro, sans-serif; 30 | color: var(--text); 31 | font-size: 1.125rem; 32 | line-height: 1.5; 33 | background-color: var(--blue); 34 | -webkit-font-smoothing: antialiased; 35 | -moz-osx-font-smoothing: grayscale; 36 | } 37 | 38 | * { 39 | box-sizing: border-box; 40 | } 41 | 42 | *:focus-visible { 43 | outline: 2px solid var(--accent); 44 | } 45 | 46 | /* ----- Typography ----- */ 47 | 48 | h1 { 49 | color: var(--black); 50 | font-size: 1.625rem; 51 | letter-spacing: -0.02em; 52 | } 53 | 54 | h2 { 55 | color: var(--black); 56 | font-size: 1.25rem; 57 | margin: 1.5em 0 0.5em; 58 | font-weight: normal; 59 | } 60 | 61 | @media (min-width: 600px) { 62 | h1 { 63 | font-size: 2rem; 64 | } 65 | 66 | h2 { 67 | font-size: 1.5rem; 68 | } 69 | } 70 | 71 | .star { 72 | color: var(--accent); 73 | display: inline-block; 74 | height: 1rem; 75 | width: 1rem; 76 | } 77 | 78 | a, 79 | a:active, 80 | a:visited, 81 | a:focus { 82 | color: var(--accent); 83 | text-decoration: none; 84 | border-radius: 2px; 85 | outline-offset: 4px; 86 | opacity: 0.75; 87 | } 88 | 89 | a:hover { 90 | opacity: 1; 91 | text-decoration: underline; 92 | } 93 | 94 | /* ----- Background helpers ----- */ 95 | 96 | .bg { 97 | background-size: 750px; 98 | background-image: url('../img/isolines.svg'); 99 | } 100 | 101 | .blue { 102 | background-color: var(--blue); 103 | } 104 | 105 | .purple { 106 | background-color: var(--purple); 107 | } 108 | 109 | .red { 110 | background-color: var(--red); 111 | } 112 | 113 | .yellow { 114 | background-color: var(--yellow); 115 | } 116 | 117 | .green { 118 | background-color: var(--green); 119 | } 120 | 121 | /* ----- Global ----- */ 122 | 123 | .container { 124 | padding: 50px 20px; 125 | max-width: var(--max-width); 126 | margin: 0 auto; 127 | } 128 | 129 | /* ----- Nav ----- */ 130 | 131 | .links { 132 | display: flex; 133 | gap: 10px; 134 | color: rgba(0, 0, 0, 0.3); 135 | font-size: 1rem; 136 | font-weight: bold; 137 | } 138 | 139 | /* ----- Bottom rail with buttons ----- */ 140 | 141 | .buttons { 142 | position: fixed; 143 | width: 100%; 144 | bottom: 0; 145 | left: 0; 146 | background: rgba(255, 255, 255, 0.2); 147 | backdrop-filter: blur(15px); 148 | -webkit-backdrop-filter: blur(15px); 149 | font-size: 1rem; 150 | box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); 151 | } 152 | 153 | .buttons-container { 154 | max-width: var(--max-width); 155 | margin: 0 auto; 156 | display: flex; 157 | flex-wrap: wrap; 158 | align-items: center; 159 | gap: 8px; 160 | padding: 10px 20px; 161 | } 162 | 163 | .buttons-scroll-to-label { 164 | font-weight: 500; 165 | flex: 0 0 100%; 166 | } 167 | 168 | @media (min-width: 600px) { 169 | .buttons-container { 170 | padding: 15px 20px; 171 | } 172 | 173 | .buttons-scroll-to-label { 174 | flex-basis: auto; 175 | } 176 | } 177 | 178 | /* ----- Buttons ----- */ 179 | 180 | button { 181 | height: 26px; 182 | border: none; 183 | background: rgba(255, 255, 255, 0.4); 184 | border-radius: 100px; 185 | padding: 0 10px; 186 | color: var(--black); 187 | font-size: 0.875rem; 188 | font-weight: 500; 189 | } 190 | 191 | button:hover { 192 | cursor: pointer; 193 | background: rgba(255, 255, 255, 0.8); 194 | } 195 | 196 | /* ----- Scroll element buttons ----- */ 197 | 198 | .button-group { 199 | display: flex; 200 | flex-direction: column; 201 | margin-bottom: 10px; 202 | gap: 4px; 203 | } 204 | 205 | .button-group div { 206 | display: flex; 207 | flex-wrap: wrap; 208 | gap: 8px; 209 | } 210 | 211 | @media (min-width: 600px) { 212 | .button-group { 213 | flex-direction: row; 214 | } 215 | 216 | .button-group b { 217 | flex: 0 0 12rem; 218 | } 219 | } 220 | 221 | /* ----- Dummy elements ----- */ 222 | 223 | .page { 224 | min-height: 1500px; 225 | } 226 | 227 | .page--text { 228 | line-height: 1500px; 229 | font-size: 6rem; 230 | text-align: center; 231 | } 232 | 233 | .page--element { 234 | display: flex; 235 | justify-content: center; 236 | align-items: center; 237 | font-size: 3rem; 238 | min-height: 100vh !important; 239 | } 240 | 241 | /* ----- Element to scroll ----- */ 242 | 243 | .element-demo { 244 | height: 300px; 245 | overflow-y: auto; 246 | background: rgba(255, 255, 255, 0.15); 247 | border-radius: 4px; 248 | margin: 20px 0; 249 | border: 2px solid rgba(255, 255, 255, 0.3); 250 | scrollbar-width: thin; 251 | scrollbar-color: var(--thumb) transparent; 252 | } 253 | 254 | .element-demo::-webkit-scrollbar { 255 | width: 8px; 256 | height: 8px; 257 | } 258 | 259 | .element-demo::-webkit-scrollbar-corner, 260 | .element-demo::-webkit-scrollbar-track { 261 | background: transparent; 262 | } 263 | 264 | .element-demo::-webkit-scrollbar-thumb { 265 | border-radius: 10px; 266 | background: var(--thumb); 267 | } 268 | 269 | .element-demo p { 270 | width: 200%; 271 | margin: 20px 0; 272 | padding: 0 20px; 273 | } 274 | -------------------------------------------------------------------------------- /docs/docs.js: -------------------------------------------------------------------------------- 1 | "use strict";(()=>{function d(t){let n=0,e=0,r=t;do n+=r.offsetTop||0,e+=r.offsetLeft||0,r=r.offsetParent;while(r);return{top:n,left:e}}var S=class{constructor(n){this.element=n}getHorizontalScroll(){return this.element.scrollLeft}getVerticalScroll(){return this.element.scrollTop}getMaxHorizontalScroll(){return this.element.scrollWidth-this.element.clientWidth}getMaxVerticalScroll(){return this.element.scrollHeight-this.element.clientHeight}getHorizontalElementScrollOffset(n,e){return d(n).left-d(e).left}getVerticalElementScrollOffset(n,e){return d(n).top-d(e).top}scrollTo(n,e){this.element.scrollLeft=n,this.element.scrollTop=e}},b=class{constructor(){this.element=window}getHorizontalScroll(){return window.scrollX||document.documentElement.scrollLeft}getVerticalScroll(){return window.scrollY||document.documentElement.scrollTop}getMaxHorizontalScroll(){return Math.max(document.body.scrollWidth,document.documentElement.scrollWidth,document.body.offsetWidth,document.documentElement.offsetWidth,document.body.clientWidth,document.documentElement.clientWidth)-window.innerWidth}getMaxVerticalScroll(){return Math.max(document.body.scrollHeight,document.documentElement.scrollHeight,document.body.offsetHeight,document.documentElement.offsetHeight,document.body.clientHeight,document.documentElement.clientHeight)-window.innerHeight}getHorizontalElementScrollOffset(n){return(window.scrollX||document.documentElement.scrollLeft)+n.getBoundingClientRect().left}getVerticalElementScrollOffset(n){return(window.scrollY||document.documentElement.scrollTop)+n.getBoundingClientRect().top}scrollTo(n,e){window.scrollTo(n,e)}},i={elements:[],cancelMethods:[],add:(t,n)=>{i.elements.push(t),i.cancelMethods.push(n)},remove:(t,n)=>{let e=i.elements.indexOf(t);e>-1&&(n&&i.cancelMethods[e](),i.elements.splice(e,1),i.cancelMethods.splice(e,1))}},U=typeof window<"u",R={cancelOnUserAction:!0,easing:t=>--t*t*t+1,elementToScroll:U?window:null,horizontalOffset:0,maxDuration:3e3,minDuration:250,speed:500,verticalOffset:0};async function X(t,n={}){if(U){if(!window.Promise)throw"Browser doesn't support Promises, and animated-scroll-to depends on it, please provide a polyfill."}else return new Promise(c=>{c(!1)});let e,r,u,o={...R,...n},f=o.elementToScroll===window,E=!!o.elementToScroll.nodeName;if(!f&&!E)throw"Element to scroll needs to be either window or DOM element.";let N=[{property:"scroll-behavior",value:"smooth"},{property:"scroll-snap-type",value:"mandatory"}],w=f?document.documentElement:o.elementToScroll,F=getComputedStyle(w);N.forEach(({property:c,value:M})=>{let a=F.getPropertyValue(c);a.includes(M)&&console.warn(`${w.tagName} has "${c}: ${a}" which can break animated-scroll-to's animations`)});let l=f?new b:new S(o.elementToScroll);if(t instanceof Element){if(u=t,E&&(!o.elementToScroll.contains(u)||o.elementToScroll.isSameNode(u)))throw"options.elementToScroll has to be a parent of scrollToElement";e=l.getHorizontalElementScrollOffset(u,o.elementToScroll),r=l.getVerticalElementScrollOffset(u,o.elementToScroll)}else if(typeof t=="number")e=l.getHorizontalScroll(),r=t;else if(Array.isArray(t)&&t.length===2)e=t[0]===null?l.getHorizontalScroll():t[0],r=t[1]===null?l.getVerticalScroll():t[1];else throw`Wrong function signature. Check documentation. 2 | Available method signatures are: 3 | animateScrollTo(y:number, options) 4 | animateScrollTo([x:number | null, y:number | null], options) 5 | animateScrollTo(scrollToElement:Element, options)`;e+=o.horizontalOffset,r+=o.verticalOffset;let T=l.getMaxHorizontalScroll(),g=l.getHorizontalScroll();e>T&&(e=T);let h=e-g,v=l.getMaxVerticalScroll(),y=l.getVerticalScroll();r>v&&(r=v);let p=r-y,H=Math.abs(Math.round(h/1e3*o.speed)),x=Math.abs(Math.round(p/1e3*o.speed)),m=H>x?H:x;return mo.maxDuration&&(m=o.maxDuration),new Promise((c,M)=>{h===0&&p===0&&c(!0),i.remove(l.element,!0);let a,A=()=>{W(),cancelAnimationFrame(a),c(!1)};i.add(l.element,A);let k=s=>s.preventDefault(),D=o.cancelOnUserAction?A:k,z=o.cancelOnUserAction?{passive:!0}:{passive:!1},O=["wheel","touchstart","keydown","mousedown"],W=()=>{O.forEach(s=>{l.element.removeEventListener(s,D,z)})};O.forEach(s=>{l.element.addEventListener(s,D,z)});let q=Date.now(),P=()=>{var s=Date.now()-q,V=s/m;let L=Math.round(g+h*o.easing(V)),I=Math.round(y+p*o.easing(V));s number;\n elementToScroll?: Element | Window;\n horizontalOffset?: number;\n maxDuration?: number;\n minDuration?: number;\n speed?: number;\n verticalOffset?: number;\n}\n\nexport interface IOptions {\n cancelOnUserAction: boolean;\n easing: (t: number) => number;\n elementToScroll: Element | Window | null;\n horizontalOffset: number;\n maxDuration: number;\n minDuration: number;\n speed: number;\n verticalOffset: number;\n}\n\n// --------- HELPERS\n\nfunction getElementOffset(el: Element) {\n let top = 0;\n let left = 0;\n let element: HTMLElement = el as HTMLElement;\n\n // Loop through the DOM tree\n // and add it's parent's offset to get page offset\n do {\n top += element.offsetTop || 0;\n left += element.offsetLeft || 0;\n element = element.offsetParent as HTMLElement;\n } while (element);\n\n return {\n top,\n left,\n };\n}\n\n// --------- SCROLL INTERFACES\n\n// ScrollDomElement and ScrollWindow have identical interfaces\n\nclass ScrollDomElement {\n element: Element;\n\n constructor(element: Element) {\n this.element = element;\n }\n\n getHorizontalScroll(): number {\n return this.element.scrollLeft;\n }\n\n getVerticalScroll(): number {\n return this.element.scrollTop;\n }\n\n getMaxHorizontalScroll(): number {\n return this.element.scrollWidth - this.element.clientWidth;\n }\n\n getMaxVerticalScroll(): number {\n return this.element.scrollHeight - this.element.clientHeight;\n }\n\n getHorizontalElementScrollOffset(\n elementToScrollTo: Element,\n elementToScroll: Element\n ): number {\n return (\n getElementOffset(elementToScrollTo).left -\n getElementOffset(elementToScroll).left\n );\n }\n\n getVerticalElementScrollOffset(\n elementToScrollTo: Element,\n elementToScroll: Element\n ): number {\n return (\n getElementOffset(elementToScrollTo).top -\n getElementOffset(elementToScroll).top\n );\n }\n\n scrollTo(x: number, y: number) {\n this.element.scrollLeft = x;\n this.element.scrollTop = y;\n }\n}\n\nclass ScrollWindow {\n element: Window = window;\n\n getHorizontalScroll(): number {\n return window.scrollX || document.documentElement.scrollLeft;\n }\n\n getVerticalScroll(): number {\n return window.scrollY || document.documentElement.scrollTop;\n }\n\n getMaxHorizontalScroll(): number {\n return (\n Math.max(\n document.body.scrollWidth,\n document.documentElement.scrollWidth,\n document.body.offsetWidth,\n document.documentElement.offsetWidth,\n document.body.clientWidth,\n document.documentElement.clientWidth\n ) - window.innerWidth\n );\n }\n\n getMaxVerticalScroll(): number {\n return (\n Math.max(\n document.body.scrollHeight,\n document.documentElement.scrollHeight,\n document.body.offsetHeight,\n document.documentElement.offsetHeight,\n document.body.clientHeight,\n document.documentElement.clientHeight\n ) - window.innerHeight\n );\n }\n\n getHorizontalElementScrollOffset(elementToScrollTo: Element): number {\n const scrollLeft = window.scrollX || document.documentElement.scrollLeft;\n return scrollLeft + elementToScrollTo.getBoundingClientRect().left;\n }\n\n getVerticalElementScrollOffset(elementToScrollTo: Element): number {\n const scrollTop = window.scrollY || document.documentElement.scrollTop;\n return scrollTop + elementToScrollTo.getBoundingClientRect().top;\n }\n\n scrollTo(x: number, y: number) {\n window.scrollTo(x, y);\n }\n}\n\n// --------- KEEPING TRACK OF ACTIVE ANIMATIONS\n\ntype ActiveAnimations = {\n elements: (Window | Element)[];\n cancelMethods: (() => void)[];\n add: (element: Element | Window, cancelAnimation: () => void) => void;\n remove: (element: Element | Window, shouldStop: boolean) => void;\n};\n\nconst activeAnimations: ActiveAnimations = {\n elements: [],\n cancelMethods: [],\n\n add: (element, cancelAnimation) => {\n activeAnimations.elements.push(element);\n activeAnimations.cancelMethods.push(cancelAnimation);\n },\n remove: (element, shouldStop) => {\n const index = activeAnimations.elements.indexOf(element);\n\n if (index > -1) {\n // Stop animation\n if (shouldStop) {\n activeAnimations.cancelMethods[index]();\n }\n // Remove it\n activeAnimations.elements.splice(index, 1);\n activeAnimations.cancelMethods.splice(index, 1);\n }\n },\n};\n\n// --------- CHECK IF CODE IS RUNNING IN A BROWSER\n\nconst WINDOW_EXISTS = typeof window !== 'undefined';\n\n// --------- ANIMATE SCROLL TO\n\nconst defaultOptions: IOptions = {\n cancelOnUserAction: true,\n easing: (t) => --t * t * t + 1, // easeOutCubic\n elementToScroll: WINDOW_EXISTS ? window : null, // Check for server side rendering\n horizontalOffset: 0,\n maxDuration: 3000,\n minDuration: 250,\n speed: 500,\n verticalOffset: 0,\n};\n\nasync function animateScrollTo(\n y: number,\n userOptions?: IUserOptions\n): Promise;\nasync function animateScrollTo(\n coords: TCoords,\n userOptions?: IUserOptions\n): Promise;\nasync function animateScrollTo(\n scrollToElement: Element,\n userOptions?: IUserOptions\n): Promise;\nasync function animateScrollTo(\n numberOrCoordsOrElement: number | TCoords | Element,\n userOptions: IUserOptions = {}\n): Promise {\n // Check for server rendering\n if (!WINDOW_EXISTS) {\n // @ts-ignore\n // If it still gets called on server, return Promise for API consistency\n return new Promise((resolve: (hasScrolledToPosition: boolean) => void) => {\n resolve(false); // Returning false on server\n });\n } else if (!(window as any).Promise) {\n throw \"Browser doesn't support Promises, and animated-scroll-to depends on it, please provide a polyfill.\";\n }\n\n let x: number;\n let y: number;\n let scrollToElement: Element;\n let options: IOptions = {\n ...defaultOptions,\n ...userOptions,\n };\n\n const isWindow = options.elementToScroll === window;\n const isElement = !!(options.elementToScroll as Element).nodeName;\n\n if (!isWindow && !isElement) {\n throw 'Element to scroll needs to be either window or DOM element.';\n }\n\n // Check for a few properties that can break the animation\n // \"scroll-behavior: smooth\"\n // \"scroll-snap-type: [x/y] mandatory\"\n // https://github.com/Stanko/animated-scroll-to/issues/55\n // https://github.com/Stanko/animated-scroll-to/issues/71\n const WARN_ABOUT = [\n { property: 'scroll-behavior', value: 'smooth' },\n { property: 'scroll-snap-type', value: 'mandatory' },\n ];\n\n const scrollBehaviorElement: Element = isWindow\n ? document.documentElement\n : (options.elementToScroll as Element);\n\n const computedStyles = getComputedStyle(scrollBehaviorElement);\n\n WARN_ABOUT.forEach(({ property, value }) => {\n const cssValue = computedStyles.getPropertyValue(property);\n\n if (cssValue.includes(value)) {\n console.warn(\n `${scrollBehaviorElement.tagName} has \"${property}: ${cssValue}\" which can break animated-scroll-to's animations`\n );\n }\n });\n\n // Select the correct scrolling interface\n const elementToScroll = isWindow\n ? new ScrollWindow()\n : new ScrollDomElement(options.elementToScroll as Element);\n\n if (numberOrCoordsOrElement instanceof Element) {\n scrollToElement = numberOrCoordsOrElement;\n\n // If \"elementToScroll\" is not a parent of \"scrollToElement\"\n if (\n isElement &&\n (!(options.elementToScroll as Element).contains(scrollToElement) ||\n (options.elementToScroll as Element).isSameNode(scrollToElement))\n ) {\n throw 'options.elementToScroll has to be a parent of scrollToElement';\n }\n\n x = elementToScroll.getHorizontalElementScrollOffset(\n scrollToElement,\n options.elementToScroll as Element\n );\n y = elementToScroll.getVerticalElementScrollOffset(\n scrollToElement,\n options.elementToScroll as Element\n );\n } else if (typeof numberOrCoordsOrElement === 'number') {\n x = elementToScroll.getHorizontalScroll();\n y = numberOrCoordsOrElement;\n } else if (\n Array.isArray(numberOrCoordsOrElement) &&\n numberOrCoordsOrElement.length === 2\n ) {\n x =\n numberOrCoordsOrElement[0] === null\n ? elementToScroll.getHorizontalScroll()\n : (numberOrCoordsOrElement[0] as number);\n y =\n numberOrCoordsOrElement[1] === null\n ? elementToScroll.getVerticalScroll()\n : (numberOrCoordsOrElement[1] as number);\n } else {\n // ERROR\n throw (\n 'Wrong function signature. Check documentation.\\n' +\n 'Available method signatures are:\\n' +\n ' animateScrollTo(y:number, options)\\n' +\n ' animateScrollTo([x:number | null, y:number | null], options)\\n' +\n ' animateScrollTo(scrollToElement:Element, options)'\n );\n }\n\n // Add offsets\n x += options.horizontalOffset;\n y += options.verticalOffset;\n\n // Horizontal scroll distance\n const maxHorizontalScroll = elementToScroll.getMaxHorizontalScroll();\n const initialHorizontalScroll = elementToScroll.getHorizontalScroll();\n\n // If user specified scroll position is greater than maximum available scroll\n if (x > maxHorizontalScroll) {\n x = maxHorizontalScroll;\n }\n\n // Calculate distance to scroll\n const horizontalDistanceToScroll = x - initialHorizontalScroll;\n\n // Vertical scroll distance distance\n const maxVerticalScroll = elementToScroll.getMaxVerticalScroll();\n const initialVerticalScroll = elementToScroll.getVerticalScroll();\n\n // If user specified scroll position is greater than maximum available scroll\n if (y > maxVerticalScroll) {\n y = maxVerticalScroll;\n }\n\n // Calculate distance to scroll\n const verticalDistanceToScroll = y - initialVerticalScroll;\n\n // Calculate duration of the scroll\n const horizontalDuration = Math.abs(\n Math.round((horizontalDistanceToScroll / 1000) * options.speed)\n );\n const verticalDuration = Math.abs(\n Math.round((verticalDistanceToScroll / 1000) * options.speed)\n );\n\n let duration =\n horizontalDuration > verticalDuration\n ? horizontalDuration\n : verticalDuration;\n\n // Set minimum and maximum duration\n if (duration < options.minDuration) {\n duration = options.minDuration;\n } else if (duration > options.maxDuration) {\n duration = options.maxDuration;\n }\n\n // @ts-ignore\n return new Promise(\n (resolve: (hasScrolledToPosition: boolean) => void, reject) => {\n // Scroll is already in place, nothing to do\n if (horizontalDistanceToScroll === 0 && verticalDistanceToScroll === 0) {\n // Resolve promise with a boolean hasScrolledToPosition set to true\n resolve(true);\n }\n\n // Cancel existing animation if it is already running on the same element\n activeAnimations.remove(elementToScroll.element, true);\n\n // To cancel animation we have to store request animation frame ID\n let requestID: number;\n\n // Cancel animation handler\n const cancelAnimation = () => {\n removeListeners();\n cancelAnimationFrame(requestID);\n\n // Resolve promise with a boolean hasScrolledToPosition set to false\n resolve(false);\n };\n\n // Registering animation so it can be canceled if function\n // gets called again on the same element\n activeAnimations.add(elementToScroll.element, cancelAnimation);\n\n // Prevent user actions handler\n const preventDefaultHandler = (e: Event) => e.preventDefault();\n\n const handler = options.cancelOnUserAction\n ? cancelAnimation\n : preventDefaultHandler;\n\n // If animation is not cancelable by the user, we can't use passive events\n const eventOptions: any = options.cancelOnUserAction\n ? { passive: true }\n : { passive: false };\n\n const events = ['wheel', 'touchstart', 'keydown', 'mousedown'];\n\n // Function to remove listeners after animation is finished\n const removeListeners = () => {\n events.forEach((eventName) => {\n elementToScroll.element.removeEventListener(\n eventName,\n handler,\n eventOptions\n );\n });\n };\n\n // Add listeners\n events.forEach((eventName) => {\n elementToScroll.element.addEventListener(\n eventName,\n handler,\n eventOptions\n );\n });\n\n // Animation\n const startingTime = Date.now();\n\n const step = () => {\n var timeDiff = Date.now() - startingTime;\n var t = timeDiff / duration;\n\n const horizontalScrollPosition = Math.round(\n initialHorizontalScroll +\n horizontalDistanceToScroll * options.easing(t)\n );\n const verticalScrollPosition = Math.round(\n initialVerticalScroll + verticalDistanceToScroll * options.easing(t)\n );\n\n if (\n timeDiff < duration &&\n (horizontalScrollPosition !== x || verticalScrollPosition !== y)\n ) {\n // If scroll didn't reach desired position or time is not elapsed\n // Scroll to a new position\n elementToScroll.scrollTo(\n horizontalScrollPosition,\n verticalScrollPosition\n );\n\n // And request a new step\n requestID = requestAnimationFrame(step);\n } else {\n // If the time elapsed or we reached the desired offset\n // Set scroll to the desired offset (when rounding made it to be off a pixel or two)\n // Clear animation frame to be sure\n elementToScroll.scrollTo(x, y);\n\n cancelAnimationFrame(requestID);\n\n // Remove listeners\n removeListeners();\n\n // Remove animation from the active animations coordinator\n activeAnimations.remove(elementToScroll.element, false);\n\n // Resolve promise with a boolean hasScrolledToPosition set to true\n resolve(true);\n }\n };\n\n // Start animating scroll\n requestID = requestAnimationFrame(step);\n }\n );\n}\n\nexport default animateScrollTo;\n", "import animateScrollTo from '../src/index';\n\n(window as any).animateScrollTo = animateScrollTo;\n"], 5 | "mappings": "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", 6 | "names": ["getElementOffset", "el", "top", "left", "element", "ScrollDomElement", "elementToScrollTo", "elementToScroll", "x", "y", "ScrollWindow", "activeAnimations", "cancelAnimation", "shouldStop", "index", "WINDOW_EXISTS", "defaultOptions", "animateScrollTo", "numberOrCoordsOrElement", "userOptions", "resolve", "scrollToElement", "options", "isWindow", "isElement", "WARN_ABOUT", "scrollBehaviorElement", "computedStyles", "property", "value", "cssValue", "maxHorizontalScroll", "initialHorizontalScroll", "horizontalDistanceToScroll", "maxVerticalScroll", "initialVerticalScroll", "verticalDistanceToScroll", "horizontalDuration", "verticalDuration", "duration", "reject", "requestID", "removeListeners", "preventDefaultHandler", "e", "handler", "eventOptions", "events", "eventName", "startingTime", "step", "timeDiff", "t", "horizontalScrollPosition", "verticalScrollPosition", "src_default", "src_default"] 7 | } 8 | -------------------------------------------------------------------------------- /docs/docs.ts: -------------------------------------------------------------------------------- 1 | import animateScrollTo from '../src/index'; 2 | 3 | (window as any).animateScrollTo = animateScrollTo; 4 | -------------------------------------------------------------------------------- /docs/img/isolines.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /docs/img/star.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Animated Scroll To 9 | 10 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 |
25 | 36 | 37 |

Animated Scroll To

38 | 39 |

40 | Lightweight (1.9kb gzipped) scroll to function with a powerful API. Scrolls window or any other DOM element. 41 | Documentation on GitHub. 42 |

43 | 44 |

45 | 46 | Window 47 |

48 |

Use buttons at the bottom of the page to scroll the page.

49 | 50 |

51 | 52 | DOM element 53 |

54 |

Use buttons beneath to scroll the div below.

55 | 56 |
57 | Scroll vertically: 58 |
59 | 60 | 62 | 64 | 66 |
67 |
68 | 69 |
70 | Scroll horizontally: 71 |
72 | 74 | 77 | 80 |
81 |
82 | 83 |
84 | Scroll diagonally: 85 |
86 | 88 | 91 | 94 |
95 |
96 | 97 |
98 | Scroll to element: 99 |
100 | 103 |
104 |
105 | 106 |
107 |

Use buttons above to scroll this element

108 |

109 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. 110 | Nullam facilisis feugiat leo vel tempor. Nullam mollis tortor quis 111 | fringilla ultricies. Vivamus eu libero malesuada orci sagittis 112 | feugiat. Nulla cursus tempor velit, vitae pellentesque ex placerat 113 | faucibus. Fusce elementum ante eget nulla condimentum, non efficitur 114 | erat venenatis. Aliquam nunc diam, tincidunt eget arcu sed, feugiat 115 | laoreet dolor. Nulla in laoreet odio. Ut nec egestas enim. Nam 116 | vestibulum pretium laoreet. Nam magna magna, dignissim volutpat augue 117 | non, imperdiet feugiat odio. 118 |

119 |

120 | Vivamus suscipit fermentum lorem, sit amet lacinia ex accumsan nec. 121 | Vivamus iaculis, neque sed auctor congue, risus ipsum laoreet odio, 122 | eu mollis dolor justo elementum dui. Praesent facilisis eleifend 123 | mattis. Aliquam ut erat dictum, aliquet ligula et, commodo felis. 124 | Nullam non venenatis lorem. Donec consectetur lectus a consectetur 125 | porta. Phasellus vel commodo tellus. Donec egestas rutrum semper. 126 |

127 |

128 | Suspendisse ac luctus velit. Proin nunc erat, placerat sit amet 129 | consectetur nec, dictum sit amet velit. Nulla semper nibh sit amet 130 | sem fermentum, pretium placerat augue rutrum. Proin tellus lorem, 131 | sodales nec lacus eu, auctor euismod neque. Praesent rutrum elit eu 132 | magna pulvinar, quis hendrerit tortor dapibus. Etiam fringilla 133 | molestie dui ut suscipit. Aenean quam nisl, imperdiet nec egestas 134 | sed, congue eu orci. Cras vehicula auctor diam at sodales. 135 | Nulla blandit faucibus ex, id lacinia neque ornare vitae. 136 | Pellentesque a felis neque. 137 |

138 |

139 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. 140 | Nullam facilisis feugiat leo vel tempor. Nullam mollis tortor quis 141 | fringilla ultricies. Vivamus eu libero malesuada orci sagittis 142 | feugiat. Nulla cursus tempor velit, vitae pellentesque ex placerat 143 | faucibus. Fusce elementum ante eget nulla condimentum, non efficitur 144 | erat venenatis. Aliquam nunc diam, tincidunt eget arcu sed, feugiat 145 | laoreet dolor. Nulla in laoreet odio. Ut nec egestas enim. Nam 146 | vestibulum pretium laoreet. Nam magna magna, dignissim volutpat augue 147 | non, imperdiet feugiat odio. 148 |

149 |

150 | Scroll to me! Vivamus suscipit fermentum lorem, sit amet lacinia ex accumsan nec. 151 | Vivamus iaculis, neque sed auctor congue, risus ipsum laoreet odio, 152 | eu mollis dolor justo elementum dui. Praesent facilisis eleifend 153 | mattis. Aliquam ut erat dictum, aliquet ligula et, commodo felis. 154 | Nullam non venenatis lorem. Donec consectetur lectus a consectetur 155 | porta. Phasellus vel commodo tellus. Donec egestas rutrum semper. 156 |

157 |

158 | Suspendisse ac luctus velit. Proin nunc erat, placerat sit amet 159 | consectetur nec, dictum sit amet velit. Nulla semper nibh sit amet 160 | sem fermentum, pretium placerat augue rutrum. Proin tellus lorem, 161 | sodales nec lacus eu, auctor euismod neque. Praesent rutrum elit eu 162 | magna pulvinar, quis hendrerit tortor dapibus. Etiam fringilla 163 | molestie dui ut suscipit. Aenean quam nisl, imperdiet nec egestas 164 | sed, congue eu orci. Cras vehicula auctor diam at sodales. 165 | Nulla blandit faucibus ex, id lacinia neque ornare vitae. 166 | Pellentesque a felis neque. 167 |

168 |

169 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. 170 | Nullam facilisis feugiat leo vel tempor. Nullam mollis tortor quis 171 | fringilla ultricies. Vivamus eu libero malesuada orci sagittis 172 | feugiat. Nulla cursus tempor velit, vitae pellentesque ex placerat 173 | faucibus. Fusce elementum ante eget nulla condimentum, non efficitur 174 | erat venenatis. Aliquam nunc diam, tincidunt eget arcu sed, feugiat 175 | laoreet dolor. Nulla in laoreet odio. Ut nec egestas enim. Nam 176 | vestibulum pretium laoreet. Nam magna magna, dignissim volutpat augue 177 | non, imperdiet feugiat odio. 178 |

179 |

180 | Vivamus suscipit fermentum lorem, sit amet lacinia ex accumsan nec. 181 | Vivamus iaculis, neque sed auctor congue, risus ipsum laoreet odio, 182 | eu mollis dolor justo elementum dui. Praesent facilisis eleifend 183 | mattis. Aliquam ut erat dictum, aliquet ligula et, commodo felis. 184 | Nullam non venenatis lorem. Donec consectetur lectus a consectetur 185 | porta. Phasellus vel commodo tellus. Donec egestas rutrum semper. 186 |

187 |

188 | Suspendisse ac luctus velit. Proin nunc erat, placerat sit amet 189 | consectetur nec, dictum sit amet velit. Nulla semper nibh sit amet 190 | sem fermentum, pretium placerat augue rutrum. Proin tellus lorem, 191 | sodales nec lacus eu, auctor euismod neque. Praesent rutrum elit eu 192 | magna pulvinar, quis hendrerit tortor dapibus. Etiam fringilla 193 | molestie dui ut suscipit. Aenean quam nisl, imperdiet nec egestas 194 | sed, congue eu orci. Cras vehicula auctor diam at sodales. 195 | Nulla blandit faucibus ex, id lacinia neque ornare vitae. 196 | Pellentesque a felis neque. 197 |

198 |

199 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. 200 | Nullam facilisis feugiat leo vel tempor. Nullam mollis tortor quis 201 | fringilla ultricies. Vivamus eu libero malesuada orci sagittis 202 | feugiat. Nulla cursus tempor velit, vitae pellentesque ex placerat 203 | faucibus. Fusce elementum ante eget nulla condimentum, non efficitur 204 | erat venenatis. Aliquam nunc diam, tincidunt eget arcu sed, feugiat 205 | laoreet dolor. Nulla in laoreet odio. Ut nec egestas enim. Nam 206 | vestibulum pretium laoreet. Nam magna magna, dignissim volutpat augue 207 | non, imperdiet feugiat odio. 208 |

209 |

210 | Vivamus suscipit fermentum lorem, sit amet lacinia ex accumsan nec. 211 | Vivamus iaculis, neque sed auctor congue, risus ipsum laoreet odio, 212 | eu mollis dolor justo elementum dui. Praesent facilisis eleifend 213 | mattis. Aliquam ut erat dictum, aliquet ligula et, commodo felis. 214 | Nullam non venenatis lorem. Donec consectetur lectus a consectetur 215 | porta. Phasellus vel commodo tellus. Donec egestas rutrum semper. 216 |

217 |

218 | Suspendisse ac luctus velit. Proin nunc erat, placerat sit amet 219 | consectetur nec, dictum sit amet velit. Nulla semper nibh sit amet 220 | sem fermentum, pretium placerat augue rutrum. Proin tellus lorem, 221 | sodales nec lacus eu, auctor euismod neque. Praesent rutrum elit eu 222 | magna pulvinar, quis hendrerit tortor dapibus. Etiam fringilla 223 | molestie dui ut suscipit. Aenean quam nisl, imperdiet nec egestas 224 | sed, congue eu orci. Cras vehicula auctor diam at sodales. 225 | Nulla blandit faucibus ex, id lacinia neque ornare vitae. 226 | Pellentesque a felis neque. 227 |

228 |
229 |
230 |
231 |
1
232 |
2
233 |
3
234 |
4
235 |
5
236 |
6
237 |
Page with element
238 |
7
239 |
8
240 |
9
241 | 242 |
243 |
244 |
Scroll window to:
245 | 246 | 247 | 248 | 249 | 250 | 251 |
252 |
253 | 254 | 255 | 256 | 257 | 258 | -------------------------------------------------------------------------------- /docs/test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Animated window scroll to 9 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 |
66 |
67 |
68 |
69 |
70 |
71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "animated-scroll-to", 3 | "version": "2.3.2", 4 | "description": "Simple, plain JavaScript animated window scroll", 5 | "main": "dist/cjs/index.js", 6 | "module": "dist/esm/index.js", 7 | "types": "dist/esm/index.d.ts", 8 | "exports": { 9 | ".": { 10 | "import": "./dist/esm/index.js", 11 | "require": "./dist/cjs/index.js" 12 | } 13 | }, 14 | "scripts": { 15 | "clean": "del-cli ./dist", 16 | "build:esm": "tsc -p tsconfig.json && echo '{ \"type\": \"module\" }' > dist/esm/package.json", 17 | "build:cjs": "tsc -p tsconfig-cjs.json && echo '{ \"type\": \"commonjs\" }' > dist/cjs/package.json", 18 | "build": "npm run clean && npm run build:esm && npm run build:cjs && npm run build:docs", 19 | "prepublishOnly": "npm test && npm run build", 20 | "start": "esbuild docs/docs.ts --bundle --tsconfig=tsconfig-demo.json --servedir=docs --serve=0.0.0.0:8000 --outdir=docs/animated-scroll-to", 21 | "build:docs": "esbuild docs/docs.ts --bundle --tsconfig=tsconfig-demo.json --outdir=docs --minify --sourcemap", 22 | "test": "node test.js", 23 | "cypress:run": "cypress run", 24 | "cypress:open": "cypress open" 25 | }, 26 | "repository": { 27 | "type": "git", 28 | "url": "git+https://github.com/Stanko/animated-scroll-to.git" 29 | }, 30 | "keywords": [ 31 | "scroll", 32 | "scrollTo" 33 | ], 34 | "author": "Stanko", 35 | "license": "MIT", 36 | "bugs": { 37 | "url": "https://github.com/Stanko/animated-scroll-to/issues" 38 | }, 39 | "homepage": "https://github.com/Stanko/animated-scroll-to#readme", 40 | "devDependencies": { 41 | "@types/node": "^22.10.7", 42 | "cypress": "^13.17.0", 43 | "del-cli": "^6.0.0", 44 | "esbuild": "^0.24.2", 45 | "typescript": "^5.7.3" 46 | }, 47 | "files": [ 48 | "dist/", 49 | "CHANGELOG.md" 50 | ] 51 | } -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export type TCoords = [number | null | undefined, number | null | undefined]; 2 | 3 | export interface IUserOptions { 4 | cancelOnUserAction?: boolean; 5 | easing?: (t: number) => number; 6 | elementToScroll?: Element | Window; 7 | horizontalOffset?: number; 8 | maxDuration?: number; 9 | minDuration?: number; 10 | speed?: number; 11 | verticalOffset?: number; 12 | } 13 | 14 | export interface IOptions { 15 | cancelOnUserAction: boolean; 16 | easing: (t: number) => number; 17 | elementToScroll: Element | Window | null; 18 | horizontalOffset: number; 19 | maxDuration: number; 20 | minDuration: number; 21 | speed: number; 22 | verticalOffset: number; 23 | } 24 | 25 | // --------- HELPERS 26 | 27 | function getElementOffset(el: Element) { 28 | let top = 0; 29 | let left = 0; 30 | let element: HTMLElement = el as HTMLElement; 31 | 32 | // Loop through the DOM tree 33 | // and add it's parent's offset to get page offset 34 | do { 35 | top += element.offsetTop || 0; 36 | left += element.offsetLeft || 0; 37 | element = element.offsetParent as HTMLElement; 38 | } while (element); 39 | 40 | return { 41 | top, 42 | left, 43 | }; 44 | } 45 | 46 | // --------- SCROLL INTERFACES 47 | 48 | // ScrollDomElement and ScrollWindow have identical interfaces 49 | 50 | class ScrollDomElement { 51 | element: Element; 52 | 53 | constructor(element: Element) { 54 | this.element = element; 55 | } 56 | 57 | getHorizontalScroll(): number { 58 | return this.element.scrollLeft; 59 | } 60 | 61 | getVerticalScroll(): number { 62 | return this.element.scrollTop; 63 | } 64 | 65 | getMaxHorizontalScroll(): number { 66 | return this.element.scrollWidth - this.element.clientWidth; 67 | } 68 | 69 | getMaxVerticalScroll(): number { 70 | return this.element.scrollHeight - this.element.clientHeight; 71 | } 72 | 73 | getHorizontalElementScrollOffset( 74 | elementToScrollTo: Element, 75 | elementToScroll: Element 76 | ): number { 77 | return ( 78 | getElementOffset(elementToScrollTo).left - 79 | getElementOffset(elementToScroll).left 80 | ); 81 | } 82 | 83 | getVerticalElementScrollOffset( 84 | elementToScrollTo: Element, 85 | elementToScroll: Element 86 | ): number { 87 | return ( 88 | getElementOffset(elementToScrollTo).top - 89 | getElementOffset(elementToScroll).top 90 | ); 91 | } 92 | 93 | scrollTo(x: number, y: number) { 94 | this.element.scrollLeft = x; 95 | this.element.scrollTop = y; 96 | } 97 | } 98 | 99 | class ScrollWindow { 100 | element: Window = window; 101 | 102 | getHorizontalScroll(): number { 103 | return window.scrollX || document.documentElement.scrollLeft; 104 | } 105 | 106 | getVerticalScroll(): number { 107 | return window.scrollY || document.documentElement.scrollTop; 108 | } 109 | 110 | getMaxHorizontalScroll(): number { 111 | return ( 112 | Math.max( 113 | document.body.scrollWidth, 114 | document.documentElement.scrollWidth, 115 | document.body.offsetWidth, 116 | document.documentElement.offsetWidth, 117 | document.body.clientWidth, 118 | document.documentElement.clientWidth 119 | ) - window.innerWidth 120 | ); 121 | } 122 | 123 | getMaxVerticalScroll(): number { 124 | return ( 125 | Math.max( 126 | document.body.scrollHeight, 127 | document.documentElement.scrollHeight, 128 | document.body.offsetHeight, 129 | document.documentElement.offsetHeight, 130 | document.body.clientHeight, 131 | document.documentElement.clientHeight 132 | ) - window.innerHeight 133 | ); 134 | } 135 | 136 | getHorizontalElementScrollOffset(elementToScrollTo: Element): number { 137 | const scrollLeft = window.scrollX || document.documentElement.scrollLeft; 138 | return scrollLeft + elementToScrollTo.getBoundingClientRect().left; 139 | } 140 | 141 | getVerticalElementScrollOffset(elementToScrollTo: Element): number { 142 | const scrollTop = window.scrollY || document.documentElement.scrollTop; 143 | return scrollTop + elementToScrollTo.getBoundingClientRect().top; 144 | } 145 | 146 | scrollTo(x: number, y: number) { 147 | window.scrollTo(x, y); 148 | } 149 | } 150 | 151 | // --------- KEEPING TRACK OF ACTIVE ANIMATIONS 152 | 153 | type ActiveAnimations = { 154 | elements: (Window | Element)[]; 155 | cancelMethods: (() => void)[]; 156 | add: (element: Element | Window, cancelAnimation: () => void) => void; 157 | remove: (element: Element | Window, shouldStop: boolean) => void; 158 | }; 159 | 160 | const activeAnimations: ActiveAnimations = { 161 | elements: [], 162 | cancelMethods: [], 163 | 164 | add: (element, cancelAnimation) => { 165 | activeAnimations.elements.push(element); 166 | activeAnimations.cancelMethods.push(cancelAnimation); 167 | }, 168 | remove: (element, shouldStop) => { 169 | const index = activeAnimations.elements.indexOf(element); 170 | 171 | if (index > -1) { 172 | // Stop animation 173 | if (shouldStop) { 174 | activeAnimations.cancelMethods[index](); 175 | } 176 | // Remove it 177 | activeAnimations.elements.splice(index, 1); 178 | activeAnimations.cancelMethods.splice(index, 1); 179 | } 180 | }, 181 | }; 182 | 183 | // --------- CHECK IF CODE IS RUNNING IN A BROWSER 184 | 185 | const WINDOW_EXISTS = typeof window !== 'undefined'; 186 | 187 | // --------- ANIMATE SCROLL TO 188 | 189 | const defaultOptions: IOptions = { 190 | cancelOnUserAction: true, 191 | easing: (t) => --t * t * t + 1, // easeOutCubic 192 | elementToScroll: WINDOW_EXISTS ? window : null, // Check for server side rendering 193 | horizontalOffset: 0, 194 | maxDuration: 3000, 195 | minDuration: 250, 196 | speed: 500, 197 | verticalOffset: 0, 198 | }; 199 | 200 | async function animateScrollTo( 201 | y: number, 202 | userOptions?: IUserOptions 203 | ): Promise; 204 | async function animateScrollTo( 205 | coords: TCoords, 206 | userOptions?: IUserOptions 207 | ): Promise; 208 | async function animateScrollTo( 209 | scrollToElement: Element, 210 | userOptions?: IUserOptions 211 | ): Promise; 212 | async function animateScrollTo( 213 | numberOrCoordsOrElement: number | TCoords | Element, 214 | userOptions: IUserOptions = {} 215 | ): Promise { 216 | // Check for server rendering 217 | if (!WINDOW_EXISTS) { 218 | // @ts-ignore 219 | // If it still gets called on server, return Promise for API consistency 220 | return new Promise((resolve: (hasScrolledToPosition: boolean) => void) => { 221 | resolve(false); // Returning false on server 222 | }); 223 | } else if (!(window as any).Promise) { 224 | throw "Browser doesn't support Promises, and animated-scroll-to depends on it, please provide a polyfill."; 225 | } 226 | 227 | let x: number; 228 | let y: number; 229 | let scrollToElement: Element; 230 | let options: IOptions = { 231 | ...defaultOptions, 232 | ...userOptions, 233 | }; 234 | 235 | const isWindow = options.elementToScroll === window; 236 | const isElement = !!(options.elementToScroll as Element).nodeName; 237 | 238 | if (!isWindow && !isElement) { 239 | throw 'Element to scroll needs to be either window or DOM element.'; 240 | } 241 | 242 | // Check for a few properties that can break the animation 243 | // "scroll-behavior: smooth" 244 | // "scroll-snap-type: [x/y] mandatory" 245 | // https://github.com/Stanko/animated-scroll-to/issues/55 246 | // https://github.com/Stanko/animated-scroll-to/issues/71 247 | const WARN_ABOUT = [ 248 | { property: 'scroll-behavior', value: 'smooth' }, 249 | { property: 'scroll-snap-type', value: 'mandatory' }, 250 | ]; 251 | 252 | const scrollBehaviorElement: Element = isWindow 253 | ? document.documentElement 254 | : (options.elementToScroll as Element); 255 | 256 | const computedStyles = getComputedStyle(scrollBehaviorElement); 257 | 258 | WARN_ABOUT.forEach(({ property, value }) => { 259 | const cssValue = computedStyles.getPropertyValue(property); 260 | 261 | if (cssValue.includes(value)) { 262 | console.warn( 263 | `${scrollBehaviorElement.tagName} has "${property}: ${cssValue}" which can break animated-scroll-to's animations` 264 | ); 265 | } 266 | }); 267 | 268 | // Select the correct scrolling interface 269 | const elementToScroll = isWindow 270 | ? new ScrollWindow() 271 | : new ScrollDomElement(options.elementToScroll as Element); 272 | 273 | if (numberOrCoordsOrElement instanceof Element) { 274 | scrollToElement = numberOrCoordsOrElement; 275 | 276 | // If "elementToScroll" is not a parent of "scrollToElement" 277 | if ( 278 | isElement && 279 | (!(options.elementToScroll as Element).contains(scrollToElement) || 280 | (options.elementToScroll as Element).isSameNode(scrollToElement)) 281 | ) { 282 | throw 'options.elementToScroll has to be a parent of scrollToElement'; 283 | } 284 | 285 | x = elementToScroll.getHorizontalElementScrollOffset( 286 | scrollToElement, 287 | options.elementToScroll as Element 288 | ); 289 | y = elementToScroll.getVerticalElementScrollOffset( 290 | scrollToElement, 291 | options.elementToScroll as Element 292 | ); 293 | } else if (typeof numberOrCoordsOrElement === 'number') { 294 | x = elementToScroll.getHorizontalScroll(); 295 | y = numberOrCoordsOrElement; 296 | } else if ( 297 | Array.isArray(numberOrCoordsOrElement) && 298 | numberOrCoordsOrElement.length === 2 299 | ) { 300 | x = 301 | numberOrCoordsOrElement[0] === null 302 | ? elementToScroll.getHorizontalScroll() 303 | : (numberOrCoordsOrElement[0] as number); 304 | y = 305 | numberOrCoordsOrElement[1] === null 306 | ? elementToScroll.getVerticalScroll() 307 | : (numberOrCoordsOrElement[1] as number); 308 | } else { 309 | // ERROR 310 | throw ( 311 | 'Wrong function signature. Check documentation.\n' + 312 | 'Available method signatures are:\n' + 313 | ' animateScrollTo(y:number, options)\n' + 314 | ' animateScrollTo([x:number | null, y:number | null], options)\n' + 315 | ' animateScrollTo(scrollToElement:Element, options)' 316 | ); 317 | } 318 | 319 | // Add offsets 320 | x += options.horizontalOffset; 321 | y += options.verticalOffset; 322 | 323 | // Horizontal scroll distance 324 | const maxHorizontalScroll = elementToScroll.getMaxHorizontalScroll(); 325 | const initialHorizontalScroll = elementToScroll.getHorizontalScroll(); 326 | 327 | // If user specified scroll position is greater than maximum available scroll 328 | if (x > maxHorizontalScroll) { 329 | x = maxHorizontalScroll; 330 | } 331 | 332 | // Calculate distance to scroll 333 | const horizontalDistanceToScroll = x - initialHorizontalScroll; 334 | 335 | // Vertical scroll distance distance 336 | const maxVerticalScroll = elementToScroll.getMaxVerticalScroll(); 337 | const initialVerticalScroll = elementToScroll.getVerticalScroll(); 338 | 339 | // If user specified scroll position is greater than maximum available scroll 340 | if (y > maxVerticalScroll) { 341 | y = maxVerticalScroll; 342 | } 343 | 344 | // Calculate distance to scroll 345 | const verticalDistanceToScroll = y - initialVerticalScroll; 346 | 347 | // Calculate duration of the scroll 348 | const horizontalDuration = Math.abs( 349 | Math.round((horizontalDistanceToScroll / 1000) * options.speed) 350 | ); 351 | const verticalDuration = Math.abs( 352 | Math.round((verticalDistanceToScroll / 1000) * options.speed) 353 | ); 354 | 355 | let duration = 356 | horizontalDuration > verticalDuration 357 | ? horizontalDuration 358 | : verticalDuration; 359 | 360 | // Set minimum and maximum duration 361 | if (duration < options.minDuration) { 362 | duration = options.minDuration; 363 | } else if (duration > options.maxDuration) { 364 | duration = options.maxDuration; 365 | } 366 | 367 | // @ts-ignore 368 | return new Promise( 369 | (resolve: (hasScrolledToPosition: boolean) => void, reject) => { 370 | // Scroll is already in place, nothing to do 371 | if (horizontalDistanceToScroll === 0 && verticalDistanceToScroll === 0) { 372 | // Resolve promise with a boolean hasScrolledToPosition set to true 373 | resolve(true); 374 | } 375 | 376 | // Cancel existing animation if it is already running on the same element 377 | activeAnimations.remove(elementToScroll.element, true); 378 | 379 | // To cancel animation we have to store request animation frame ID 380 | let requestID: number; 381 | 382 | // Cancel animation handler 383 | const cancelAnimation = () => { 384 | removeListeners(); 385 | cancelAnimationFrame(requestID); 386 | 387 | // Resolve promise with a boolean hasScrolledToPosition set to false 388 | resolve(false); 389 | }; 390 | 391 | // Registering animation so it can be canceled if function 392 | // gets called again on the same element 393 | activeAnimations.add(elementToScroll.element, cancelAnimation); 394 | 395 | // Prevent user actions handler 396 | const preventDefaultHandler = (e: Event) => e.preventDefault(); 397 | 398 | const handler = options.cancelOnUserAction 399 | ? cancelAnimation 400 | : preventDefaultHandler; 401 | 402 | // If animation is not cancelable by the user, we can't use passive events 403 | const eventOptions: any = options.cancelOnUserAction 404 | ? { passive: true } 405 | : { passive: false }; 406 | 407 | const events = ['wheel', 'touchstart', 'keydown', 'mousedown']; 408 | 409 | // Function to remove listeners after animation is finished 410 | const removeListeners = () => { 411 | events.forEach((eventName) => { 412 | elementToScroll.element.removeEventListener( 413 | eventName, 414 | handler, 415 | eventOptions 416 | ); 417 | }); 418 | }; 419 | 420 | // Add listeners 421 | events.forEach((eventName) => { 422 | elementToScroll.element.addEventListener( 423 | eventName, 424 | handler, 425 | eventOptions 426 | ); 427 | }); 428 | 429 | // Animation 430 | const startingTime = Date.now(); 431 | 432 | const step = () => { 433 | var timeDiff = Date.now() - startingTime; 434 | var t = timeDiff / duration; 435 | 436 | const horizontalScrollPosition = Math.round( 437 | initialHorizontalScroll + 438 | horizontalDistanceToScroll * options.easing(t) 439 | ); 440 | const verticalScrollPosition = Math.round( 441 | initialVerticalScroll + verticalDistanceToScroll * options.easing(t) 442 | ); 443 | 444 | if ( 445 | timeDiff < duration && 446 | (horizontalScrollPosition !== x || verticalScrollPosition !== y) 447 | ) { 448 | // If scroll didn't reach desired position or time is not elapsed 449 | // Scroll to a new position 450 | elementToScroll.scrollTo( 451 | horizontalScrollPosition, 452 | verticalScrollPosition 453 | ); 454 | 455 | // And request a new step 456 | requestID = requestAnimationFrame(step); 457 | } else { 458 | // If the time elapsed or we reached the desired offset 459 | // Set scroll to the desired offset (when rounding made it to be off a pixel or two) 460 | // Clear animation frame to be sure 461 | elementToScroll.scrollTo(x, y); 462 | 463 | cancelAnimationFrame(requestID); 464 | 465 | // Remove listeners 466 | removeListeners(); 467 | 468 | // Remove animation from the active animations coordinator 469 | activeAnimations.remove(elementToScroll.element, false); 470 | 471 | // Resolve promise with a boolean hasScrolledToPosition set to true 472 | resolve(true); 473 | } 474 | }; 475 | 476 | // Start animating scroll 477 | requestID = requestAnimationFrame(step); 478 | } 479 | ); 480 | } 481 | 482 | export default animateScrollTo; 483 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | const { spawn } = require('node:child_process'); 2 | 3 | const server = spawn('npm', ['run', 'start']); 4 | 5 | server.stdout.on('data', (data) => { 6 | console.log(`server stdout: ${data}`); 7 | 8 | const cypress = spawn('npm', ['run', 'cypress:run'], { stdio: 'inherit' }); 9 | 10 | cypress.on('close', (code) => { 11 | console.log(`cypress process exited with code ${code}`); 12 | server.exitCode = 0; 13 | server.kill('SIGINT'); 14 | }); 15 | }); 16 | 17 | // server.stderr.on('data', (data) => { 18 | // console.error(`server stderr: ${data}`); 19 | // }); 20 | 21 | server.on('close', (code) => { 22 | console.log(`server process exited with code ${code}`); 23 | }); 24 | -------------------------------------------------------------------------------- /tsconfig-base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "esModuleInterop": true, 5 | "declaration": true, 6 | "jsx": "react", 7 | "module": "es6", 8 | "moduleResolution": "node", 9 | "outDir": "dist", 10 | "pretty": true, 11 | "resolveJsonModule": true, 12 | "strict": true 13 | }, 14 | "exclude": ["node_modules", "dist"], 15 | "include": ["src"] 16 | } 17 | -------------------------------------------------------------------------------- /tsconfig-cjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig-base.json", 3 | "compilerOptions": { 4 | "module": "commonjs", 5 | "outDir": "dist/cjs", 6 | "target": "es2015" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /tsconfig-demo.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "esModuleInterop": true, 5 | "declaration": true, 6 | "jsx": "react", 7 | "moduleResolution": "node", 8 | "pretty": true, 9 | "resolveJsonModule": true, 10 | "strict": true, 11 | "module": "commonjs", 12 | "outDir": "docs", 13 | "target": "es2015" 14 | }, 15 | "exclude": ["node_modules", "dist"] 16 | } 17 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig-base.json", 3 | "compilerOptions": { 4 | "module": "ES2020", 5 | "outDir": "dist/esm", 6 | "target": "ES2015" 7 | } 8 | } 9 | --------------------------------------------------------------------------------