Last active
March 17, 2022 19:10
-
-
Save vinaypuppal/b7271ad84a0d69c9cfafaaa83afed199 to your computer and use it in GitHub Desktop.
Next.js smooth scroll
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React, { Children } from 'react' | |
import Router from 'next/router' | |
import smoothScroll from '../utils/smoothScroll' | |
// this HOC is taken from https://github.com/zeit/next.js/blob/master/lib/link.js and modified | |
export default class LinkSmoothScroll extends React.Component { | |
constructor (props) { | |
super(props) | |
this.linkClicked = this.linkClicked.bind(this) | |
} | |
linkClicked (e) { | |
e.preventDefault() | |
Router | |
.push(this.props.href) | |
.then(() => { | |
return smoothScroll(this.props.href) | |
}) | |
.then(() => { | |
this.props.done && this.props.done() | |
}) | |
.catch(err => { | |
this.props.onError && this.props.onError(err) | |
console.error(err) | |
}) | |
} | |
render () { | |
let { children } = this.props | |
if (typeof children === 'string') { | |
children = <a>{children}</a> | |
} | |
const child = Children.only(children) | |
const props = { onClick: this.linkClicked } | |
if (child.type === 'a' && !('href' in child.props)) { | |
props.href = this.props.href | |
} | |
return React.cloneElement(child, props) | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Get the top position of an element in the document | |
const getTop = function (element, start) { | |
// return value of html.getBoundingClientRect().top ... IE : 0, other browsers : -pageYOffset | |
if (element.nodeName === 'HTML') return -start | |
return element.getBoundingClientRect().top + start | |
} | |
// ease in out function thanks to: | |
// http://blog.greweb.fr/2012/02/bezier-curve-based-easing-functions-from-concept-to-implementation/ | |
const easeInOutCubic = function (t) { | |
return t < 0.5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1 | |
} | |
// calculate the scroll position we should be in | |
// given the start and end point of the scroll | |
// the time elapsed from the beginning of the scroll | |
// and the total duration of the scroll (default 500ms) | |
const position = function (start, end, elapsed, duration) { | |
if (elapsed > duration) return end | |
return start + | |
(end - start) * | |
easeInOutCubic( | |
elapsed / duration | |
) // <-- you can change the easing funtion there | |
// return start + (end - start) * (elapsed / duration); // <-- this would give a linear scroll | |
} | |
// we use requestAnimationFrame to be called by the browser before every repaint | |
// if the first argument is an element then scroll to the top of this element | |
// if the first argument is numeric then scroll to this location | |
// if the callback exist, it is called when the scrolling is finished | |
// if context is set then scroll that element, else scroll window | |
const smoothScroll = function (el, duration, callback, context) { | |
duration = duration || 500 | |
context = context || window | |
const start = context.scrollTop || window.pageYOffset | |
let end | |
if (typeof el === 'number') { | |
end = parseInt(el) - 60 | |
} else { | |
end = getTop(el, start) - 60 | |
} | |
const clock = Date.now() | |
const requestAnimationFrame = window.requestAnimationFrame || | |
window.mozRequestAnimationFrame || | |
window.webkitRequestAnimationFrame || | |
function (fn) { | |
window.setTimeout(fn, 15) | |
} | |
const step = function () { | |
const elapsed = Date.now() - clock | |
if (context !== window) { | |
context.scrollTop = position(start, end, elapsed, duration) | |
} else { | |
window.scroll(0, position(start, end, elapsed, duration)) | |
} | |
if (elapsed > duration) { | |
if (typeof callback === 'function') { | |
callback(el) | |
} | |
} else { | |
requestAnimationFrame(step) | |
} | |
} | |
step() | |
} | |
export default url => { | |
return new Promise(function (resolve, reject) { | |
const pattern = /^(\/#.+)|(.+(\/#.+))$/ | |
if (pattern.test(url)) { | |
const hash = pattern.exec(url).filter(item => item).pop() | |
const id = hash.replace(/\/?#/, '') | |
const el = document.getElementById(id) | |
if (el) { | |
smoothScroll(el, 600, resolve) | |
return | |
} | |
} | |
reject(new Error('Error: hash in URL is invalid or element not found!')) | |
}) | |
} |
This is not working at all. Its just as same as using Link from next/link
It's not calling this one:
//#LinkSmoothScroll.js
linkClicked(e) {
e.preventDefault()
Router
.push(this.props.href)
.then(() => {
console.log('test') //this one is not being called at all when I click the <LinkSmoothScroll>
return smoothScroll(this.props.href)
})
.then(() => {
this.props.done && this.props.done()
})
.catch(err => {
this.props.onError && this.props.onError(err)
console.error(err)
})
}
That row is being called only when the location changes. So when I change only #
in the route, that row does not gets executed.
I came up with this solution, since when you are in the same page, you don't need to wait for Router.push()
.
inkClicked(e) {
e.preventDefault();
const scrollX = window.pageXOffset;
const scrollY = window.pageYOffset;
const location = window.location;
const href = this.props.href;
if (location.pathname === href.split('#')[0]) {
Router.push(this.props.href);
window.scrollTo(scrollX, scrollY);
return smoothScroll(this.props.href)
}
else {
Router
.push(this.props.href)
.then(() => {
window.scrollTo(scrollX, scrollY);
return smoothScroll(this.props.href)
})
.then(() => {
this.props.done && this.props.done()
})
.catch(err => {
this.props.onError && this.props.onError(err)
console.error(err)
})
}
}
So if you are in the same location, it will just move to that id, otherwise it will load page and then move it.
You can also add this style to the page:
<style global jsx>
{` html { scroll-behavior: smooth; }`}
</style>
<style global jsx> {` html { scroll-behavior: smooth; }`} </style>
This just works
This is meant to work on all browsers. The scroll-behavior: smooth
approach works on all major browsers except Safari.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Hi,
Nice gist !
Smooth scrolling was not working as expected for me.
I think it is because Router.push calls internally scrollIntoView(), so I did this fix (a bit ugly but it works) :