Working on a feature for # 🚀 Feature: Add a 'back to top' button for long search results. My first attempt was close, my approach was along the lines of:
- Add a link to the page with an
href="#title"
- The button has an
onClick
that:- Takes the clicked link’s
href
, removes the#
, and finds an element by that ID (so,"title"
) - Calls
window.scrollTo
with atop
equal to that element’s bounding client rectangle’s top
- Takes the clicked link’s
<Link href="#title" onClick={(event) => { event.preventDefault(); const targetID = event.currentTarget.href.replace(/.+#/, ""); const element = document.getElementById(targetId); window.scrollTo({ behavior: "smooth", top: element?.getBoundingClientRec().top }); }) > Scroll to Top </Link>
Streamlining the title
Rather than constructing the title from the event.currentTarget,
! can keep a constant in your code. That way it never has to re-read it from the DOM element.
const targetID = "title";
<Link href={`#${targetID}`}
onClick={(event) => { event.preventDefault(); const element = document.getElementById(targetId); window.scrollTo({ behavior: "smooth", top: element?.getBoundingClientRec().top }); }) > Scroll to Top
</Link>
Re-using constants instead of accessing dynamic DOM attributes is generally a good idea. It’s less code you have to write. And it means you don’t have to risk reaching into the document (which users might have toyed with).
An alternative: React Refs
In theory, the code could avoid the getElementById
call altogether by using a React “ref” (reference to some information separate from your React state). Refs can be used for a few things, so there are a couple of docs pages:
- Referencing values with Refs
- Manipulating the DOM with Refs
Specifically, what can happen here is use a DOM reference to store the HTML
<h1>
in state, then pass it to theResultsDisplay
component as a prop:
const [title, setTitle] = useState<HTMLHeadingElement | undefined>(); // ...
return (
<>
{" "}
<h1 ref={(element) => setTitle(element || undefined)}>...</h1> <ResultsDisplay title={title} />
</>
);
Then, ResultsDisplay
could call the element’s .scrollIntoView
:
<Link onClick={() => { title?.scrollIntoView(); });
However this only works on Next.js client components, therefore:
Streamlining the Scroll
This can be achieved with even less code by avoiding #title
altogether. Calling document.body.scrollTo
and telling it top: 0
will scroll to the top of the <body>
element.
<Link href={`#${targetID}`} onClick={(event) => { event.preventDefault(); document.body.scrollTo({ behavior: "smooth", top: 0 }); }) > Scroll to Top </Link>
In this instance I need to use document.body.scrollTo
rather than window.scrollTo
as the <body>
element has a vertical scrollbar. The <body>
is set to height: 100%
of the containing html
object, using document.body.scrollTo
will take us to 0% (the top) of the body object.
<button onClick={() => { document.body.scrollTo({ behavior: "smooth", top: 0 }); }) > Scroll to Top </button>
Adding some styling
We only want the Scroll to Top button to appear once we've scrolled away from the top of the page. To implement this, first we need to set the initial state of the button:
const [visible, setVisible] = useState(false);
Then we need to create the function that will switch the visibility on and off:
const toggleVisible = () => {
const scrolled = document.body.scrollTop;
setVisible(scrolled > 100);
};
Finally, we add the event lister that keeps track of how far we've scrolled down the page:
if (typeof document !== "undefined") {
document.body.addEventListener("scroll", toggleVisible, { passive: true });
}
The event listener listens for the "scroll"
event and notifies the toggleVisible
function. We set the function to passive: true
to improve performance, which you can read more about here.
Hope this helps! Happy coding!