TIL: position: sticky and Scrolling
Last updated: Thu Apr 20 2023
For larger devices, I wanted the header of this website to be sticky - it should follow you as you scroll down the page.
I used to handle that manually, but it turns out that all you need is position: sticky
in your CSS to get that behavior for free!
Unfortunately, that messes with scrolling - if you click an anchor link on the page, it will scroll that header to the very top of the page, underneath the sticky header.
I couldn’t find a way around this using just CSS, so I asked ChatGPT how to fix it. (Update: I have since learned about scroll-padding
, which solves this nicely!) After a bit of back and forth with the LLM, this is what I ended up with:
// Scroll to anchor links, taking into account header
const header = document.querySelector("#header") as HTMLElement;
const headerHeight = header?.offsetHeight ?? 0;
const anchorLinks = document.querySelectorAll("a[href^='#']");
for (const anchorLink of anchorLinks) {
anchorLink.addEventListener("click", function (event) {
if (
window.getComputedStyle(header).getPropertyValue("position") !==
"sticky"
) {
return;
}
event.preventDefault();
const targetId = anchorLink.getAttribute("href") ?? "";
const targetPosition = (document.querySelector(targetId) as HTMLElement)
?.offsetTop;
window.scrollTo({
top: targetPosition - headerHeight,
behavior: "smooth",
});
window.history.pushState(null, "", targetId);
});
}
function scrollToAnchor() {
if (
window.getComputedStyle(header).getPropertyValue("position") !== "sticky"
) {
return;
}
var targetId = location.hash.slice(1);
if (targetId) {
var targetElement = document.getElementById(targetId);
if (targetElement) {
var targetOffset = targetElement.offsetTop - headerHeight;
window.scrollTo({
top: targetOffset,
behavior: "smooth",
});
}
}
}
window.addEventListener("hashchange", scrollToAnchor);
window.addEventListener("load", scrollToAnchor);
Whenever I click an anchor link or load the page (which might load directly to an anchor link), I need to manually scroll to the header, taking into account the height of the #header
div, but only if that div’s computed styles actually include position: sticky
. I’m not actually sure if the hashchange
event is also necessary - it’s called every time the anchor link component of the URL changes, but I think the load
event already handles that case as well.