ECMAScript2015+ & TypeScript Friendly, dependency-free smooth scroll library.
🍭 See Demo
- Dependecy-free!!
- ECMAScript2015+ & TypeScript friendly
- Use
requestAnimationFrame
API - Supports vertical and horizontal scroll
- Supports dynamic trigger (event delegation)
- Supports container for the scroll
- Supports many easing types
- Supports server-side rendering (Can load without putting out errors.)
- Usage
- Options
- Easings
- Customizing Tips
- API
- new SweetScroll(options?: PartialOptions, container?: string | Element)
- SweetScroll.create(options?: PartialOptions, container?: string | Element)
- to(distance: any, options?: PartialOptions)
- toTop(distance: any, options?: PartialOptions)
- toLeft(distance: any, options?: PartialOptions)
- toElement($el: Element, options?: PartialOptions)
- update(options: PartialOptions)
- stop(gotoEnd: boolean = true)
- destroy()
- Callbacks
- Browser Support
- CHANGELOG
- Contibute
- License
$ npm install sweet-scroll
import SweetScroll from 'sweet-scroll';
- Download the sweet-scroll.min.js
- Load it in the script tag.
<script src="sweet-scroll.min.js"></script>
<script src="https://unpkg.com/sweet-scroll/sweet-scroll.min.js"></script>
<a href="#intro" data-scroll>Go to Introduction</a>
...
<div id="intro">Introduction</div>
You need to initialize an instance after DOMContentLoaded
.
document.addEventListener('DOMContentLoaded', () => {
const scroller = new SweetScroll({/* some options */});
}, false);
The following options are applied by default. It can be customized as needed.
{
trigger: '[data-scroll]', // Selector for trigger (must be a valid css selector)
header: '[data-scroll-header]', // Selector or Element for fixed header (Selector of must be a valid css selector)
duration: 1000, // Specifies animation duration in integer
easing: 'easeOutQuint', // Specifies the pattern of easing
offset: 0, // Specifies the value to offset the scroll position in pixels
vertical: true, // Enable the vertical scroll
horizontal: false, // Enable the horizontal scroll
cancellable: true, // When fired wheel or touchstart events to stop scrolling
updateURL: false, // Update the URL hash on after scroll (true | false | 'push' | 'replace')
preventDefault: true, // Cancels the container element click event
stopPropagation: true, // Prevents further propagation of the container element click event in the bubbling phase
quickMode: false, // Instantly scroll to the destination! (It's recommended to use it with `easeOutExpo`)
// Callbacks
before: null,
after: null,
cancel: null,
complete: null,
step: null,
}
Supports the following easing.
- Normal
linear
- Quad
easeInQuad
easeOutQuad
easeInOutQuad
- Cubic
easeInCubic
easeOutCubic
easeInOutCubic
- Quart
easeInQuart
easeOutQuart
easeInOutQuart
- Quint
easeInQuint
easeOutQuint
(default)easeInOutQuint
- Sine
easeInSine
easeOutSine
easeInOutSine
- Expo
easeInExpo
easeOutExpo
easeInOutExpo
- Circ
easeInCirc
easeOutCirc
easeInOutCirc
Easing functions that are not built in can pass functions directly.
const scroller = new SweetScroll({
easing: advancedEasingFunction,
});
easeInElastic
const easeInElastic = (_, t, b, c, d) => {
let s = 1.70158;
let p = 0;
let a = c;
if (t === 0) return b;
if ((t /= d) === 1) return b + c;
if (!p) p = d * 0.3;
if (a < Math.abs(c)) {
a = c;
s = p / 4;
} else {
s = p / (2 * Math.PI) * asin(c / a);
}
return -(a * Math.pow(2, 10 * (t -= 1)) * Math.sin((t * d - s) * (2 * Math.PI) / p)) + b;
};
easeOutElastic
const easeOutElastic = (_, t, b, c, d) => {
let s = 1.70158;
let p = 0;
let a = c;
if (t === 0) return b;
if ((t /= d) === 1) return b + c;
if (!p) p = d * 0.3;
if (a < Math.abs(c)) {
a = c;
s = p / 4;
} else {
s = p / (2 * Math.PI) * asin(c / a);
}
return a * Math.pow(2, -10 * t) * Math.sin((t * d - s) * (2 * Math.PI) / p) + c + b;
};
easeInOutElastic
const easeInOutElastic = (_, t, b, c, d) => {
let s = 1.70158;
let p = 0;
let a = c;
if (t === 0) return b;
if ((t /= d / 2) === 2) return b + c;
if (!p) p = d * (0.3 * 1.5);
if (a < Math.abs(c)) {
a = c;
s = p / 4;
} else {
s = p / (2 * Math.PI) * Math.asin(c / a);
}
if (t < 1) {
return -0.5 * (a * Math.pow(2, 10 * (t -= 1)) * Math.sin((t * d - s) * (2 * Math.PI) / p)) + b;
}
return a * Math.pow(2, -10 * (t -= 1)) * Math.sin((t * d - s) * (2 * Math.PI) / p) * 0.5 + c + b;
};
easeInBack
const easeInBack = (_, t, b, c, d, s = 1.70158) => (
c * (t /= d) * t * ((s + 1) * t - s) + b
);
easeOutBack
const easeOutBack = (_, t, b, c, d, s = 1.70158) => (
c * ((t = t / d - 1) * t * ((s + 1) * t + s) + 1) + b
);
easeInOutBack
const easeInOutBack = (_, t, b, c, d, s = 1.70158) => (
(t /= d / 2) < 1
? c / 2 * (t * t * (((s *= (1.525)) + 1) * t - s)) + b
: c / 2 * ((t -= 2) * t * (((s *= (1.525)) + 1) * t + s) + 2) + b
);
easeOutBounce
const easeOutBounce = (_, t, b, c, d) => {
if ((t /= d) < (1 / 2.75)) {
return c * (7.5625 * t * t) + b;
} else if (t < (2 / 2.75)) {
return c * (7.5625 * (t -= (1.5 / 2.75)) * t + 0.75) + b;
} else if (t < (2.5 / 2.75)) {
return c * (7.5625 * (t -= (2.25 / 2.75)) * t + 0.9375) + b;
}
return c * (7.5625 * (t -= (2.625 / 2.75)) * t + 0.984375) + b;
};
easeInBounce
const easeOutBounce = (_, t, b, c, d) => {
if ((t /= d) < (1 / 2.75)) {
return c * (7.5625 * t * t) + b;
} else if (t < (2 / 2.75)) {
return c * (7.5625 * (t -= (1.5 / 2.75)) * t + 0.75) + b;
} else if (t < (2.5 / 2.75)) {
return c * (7.5625 * (t -= (2.25 / 2.75)) * t + 0.9375) + b;
}
return c * (7.5625 * (t -= (2.625 / 2.75)) * t + 0.984375) + b;
};
const easeInBounce = (x, t, b, c, d) => (
c - easeOutBounce(x, d - t, 0, c, d) + b
);
easeInOutBounce
const easeOutBounce = (_, t, b, c, d) => {
if ((t /= d) < (1 / 2.75)) {
return c * (7.5625 * t * t) + b;
} else if (t < (2 / 2.75)) {
return c * (7.5625 * (t -= (1.5 / 2.75)) * t + 0.75) + b;
} else if (t < (2.5 / 2.75)) {
return c * (7.5625 * (t -= (2.25 / 2.75)) * t + 0.9375) + b;
}
return c * (7.5625 * (t -= (2.625 / 2.75)) * t + 0.984375) + b;
};
const easeInBounce = (x, t, b, c, d) => (
c - easeOutBounce(x, d - t, 0, c, d) + b
);
const easeInOutBounce = (x, t, b, c, d) => (
t < d / 2
? easeInBounce(x, t * 2, 0, c, d) * 0.5 + b
: easeOutBounce(x, t * 2 - d, 0, c, d) * 0.5 + c * 0.5 + b
);
In the following example we have specified in the container for scrolling the #container
.
<div id="container">
<a href="#heading2" data-scroll>Go to Heading2</a>
...
<h2 id="heading2">Heading2</h2>
</div>
// Specified in the CSS Selector
const scroller = new SweetScroll({/* some options */}, '#container');
// Specified in the Element
const scroller = new SweetScroll({/* some options */}, document.getElementById('container'));
Add the data-scroll-header
attribute in order to offset the height of the fixed header.
<header data-scroll-header></header>
Specify the CSS Selector in header
option instead of the data-scroll-header
attribute.
const scroller = new SweetScroll({
header: '#header',
});
You can override the default options by passing the option in JSON
format to the data-scroll-options
.
<a href="#target" data-scroll data-scroll-options='{"easing": "easeOutExpo"}'>Go to Target</a>
Will use the data-scroll attribute instead of href.
<button type="button" data-scroll="+=500">Scroll under 500px</button>
You can solve with quickMode
options!
const scroller = new SweetScroll({
quickMode: true,
easing: 'easeOutExpo', // Recommended
});
quickMode
finishes scrolling in a moment.
The following, Introduce one of the mounting method.
document.addEventListener('DOMContentLoaded', () => {
const scroller = new SweetScroll();
const hash = window.location.hash;
const needsInitialScroll = document.getElementById(hash.substr(1)) != null;
if (needsInitialScroll) {
window.location.hash = '';
}
window.addEventListener('load', () => {
if (needsInitialScroll) {
scroller.to(hash, { updateURL: 'replace' });
}
}, false);
}, false);
You can also achieve the same thing in other ways by using the provided API.
Will generate a SweetScroll instance.
Example:
const scroller = new SweetScroll({
duration: 1200,
easing: 'easeOutExpo',
}, '#container');
Will generate a SweetScroll instance. (factory method)
Example:
const scroller = SweetScroll.create({
duration: 1200,
easing: 'easeOutExpo',
}, '#container');
Scroll animation to the specified distance
.
distance
to can specify the CSS Selector or scroll position.
Example:
const scroller = new SweetScroll();
// CSS Selector of target element
scroller.to('#footer');
// Object
scroller.to({ top: 1000, left: 20 });
// Array (top:0, left:1000)
scroller.to([0, 1000]);
// Number (Priority to vertical scroll position. by default.)
scroller.to(500);
// String (Relative position)
scroller.to('+=500');
scroller.to('-=200');
Vertical scroll animation to the specified distance
.
Example:
scroller.toTop(0);
Horizontal scroll animation to the specified distance
.
Example:
scroller.toLeft(1500);
Scroll animation to the specified Element
.
Example:
scroller.toElement(document.getElementById('content'));
Will update the SweetScroll instance.
Primarily used in the case of option update.
Example:
scroller.update({
trigger: 'a[href^="#"]',
duration: 3000,
});
gotoEnd: {Boolean}
Will stop the current scroll animation.
Example:
scroller.stop(true);
Will destroy the SweetScroll instance.
Disable of the method and event handler.
Example:
scroller.destroy();
In before
and after
, you will pass the coordinates and the triggering element in the argument.
In addition, you can stop the scrolling by return a before
in false
.
Example:
const scroller = new SweetScroll({
// Stop scrolling case of trigger element that contains the `is-disabled` class.
before: (offset: Offset, $trigger: Element | null, scroller: SweetScroll): boolean | void => {
console.log('Before!!', offset, scroller);
if ($trigger && $trigger.classList.contains('is-disabled')) {
return false;
}
},
// If the `wheel` or `touchstart` event is called
cancel: (scroller: SweetScroll): void => {
console.log('Cancel!!', scroller);
},
// Scroll animation is complete
after: (offset: Offset, $trigger: Element | null, scroller: SweetScroll): void => {
console.log('After!!', offset, $trigger, scroller);
},
// Scroll animation is complete (`after` or `cancel`)
complete: (isCancel: boolean, scroller: SweetScroll): void => {
console.log('Complete!!', isCancel, scroller);
},
// Each animation frame
step: (time: number, scroller: SweetScroll): void => {
console.log('step', time, scroller);
},
});
Extends Class:
The following is a pattern to override a method in the inheritance destination class.
import SweetScroll, { Offset } from 'sweet-scroll';
class MyScroll extends SweetScroll {
protected onBefore(offset: Offset, $trigger: Element | null): boolean | void {
// Stop scrolling case of trigger element that contains the `is-disabled` class.
console.log('Before!!', offset);
if ($trigger && $trigger.classList.contains('is-disabled')) {
return false;
}
}
protected onCancel(): void {
console.log('Canell!!');
}
protected onAfter(offset: Offset, $trigger: Element | null): void {
console.log('After!!', offset, $trigger);
}
protected onComplete(isCancel: boolean): void {
console.log('Complete!!', isCancel);
}
protected onStep(time: number): void {
console.log('step', time);
}
}
Works in IE10+
, and all modern browsers.
It is necessary to use polyfill or ponyfill of requestAnimationFrame
.
Example ponyfill
Using raf module.
import raf from 'raf';
import SweetScroll from 'sweet-scroll';
SweetScroll.raf = raf;
SweetScroll.caf = raf.cancel;
See the CHANGELOG.md
- Fork it!
- Create your feature branch:
git checkout -b my-new-feature
- Commit your changes:
git commit -am 'Add some feature'
- Push to the branch:
git push origin my-new-feature
- Submit a pull request 💪
Bugs, feature requests and comments are more than welcome in the issues.
We will develop using the following npm scripts.
Launch the local server and let the demo run. Opening http://localhost:3000 in your browser.
Compile TypeScript and create type definitions.
Run unit testing with Jest.