Simple Sticky Positioning “stuck” Styling

Published
Categories

Since we still don’t have any kind of CSS option for styling an element with position: sticky when it is actually “stuck”, I made this simple JS snippet that manages an HTML attribute that I can use in CSS to change the appearance of the element when it is currently sticking.

function handleStuckState(el) {
	const style = window.getComputedStyle(el);
	const top = parseInt(style.getPropertyValue("top"), 10);
	const rect = el.getBoundingClientRect();

	if (top === rect.y && window.scrollY > 0) {
		el.dataset.stuck = "";
	} else if (el.hasAttribute("data-stuck")) {
		el.removeAttribute("data-stuck");
	}
}



const stickyElements = document.querySelectorAll("[data-sticky]");

let requestedFrame;
window.addEventListener("scroll", (event) => {
	if (requestedFrame) {
		return;
	}
	requestedFrame = requestAnimationFrame(() => {
		if (stickyElements) {
			stickyElements.forEach((el) => {
				handleStuckState(el);
			});
		}

		requestedFrame = undefined;
	});
});Code language: JavaScript (javascript)

By checking the top property we can accurately check for elements that are not aligned to the very top of the window (such as multiple stacked sticky elements), unlike other solutions I have seen for this problem which only work if your element can be pushed off the edge of the screen slightly. We cannot use an intersection observer to check for the “stuck” state as our element will likely fully be in the viewport both before and during it’s “stuck” state, but we make sure to only update the DOM when actually needed. Additionally we limit how often it checks using requestAnimationFrame, although these days most browsers only fire the scroll event along with animation frames anyway.

Then we can use a data-sticky in our HTML to mark which elements we want it to update:

<!-- original -->
<header class="c-site-header" data-sticky=""></header>

<!-- "stuck" state active -->
<header class="c-site-header" data-sticky="" data-stuck=""></header>Code language: HTML, XML (xml)

And then simply use a [data-stuck] CSS selector to apply our styling only when the element is currently sticking.