I am trying to implement a sticky titles feature, and I think I am getting too complicated.
This is where I am right now:
class StickyTitles {
constructor(titlesSelector, containerSelector, distanceFromTopDetection) {
this.titles = Array.from(document.querySelectorAll(titlesSelector));
this.container = document.querySelector(containerSelector);
// Security buffer number to check if the intersection has happened in the top of the container
this.distanceFromTopDetection = distanceFromTopDetection;
}
start() {
this.setAllTitlesSticky();
let observer = new IntersectionObserver(this.observerCallback.bind(this), this.observerOptions());
this.titles.forEach( (e) => observer.observe(e) );
}
setAllTitlesSticky() {
this.titles.forEach((e) => {
e.style.position = "sticky";
e.style.top = "0px";
e.style.zIndex = "9999";
});
}
observerOptions() {
console.log("this.container:", this.container);
const containerPaddingTop = window.getComputedStyle(this.container).getPropertyValue("padding-top");
const containerPaddingTopInPx = parseInt(containerPaddingTop);
console.log("containerPaddingTop:", containerPaddingTop);
console.log("containerPaddingTopInPx:", containerPaddingTopInPx);
return {
root: this.container,
rootMargin: `-${containerPaddingTopInPx + 10}px 0px 0px 0px`,
threshold: [1],
};
}
observerCallback(entries, _observer) {
console.log("observerCallback");
// Each entry describes an intersection change for one observed
// target element:
// entry.boundingClientRect
// entry.intersectionRatio
// entry.intersectionRect
// entry.isIntersecting
// entry.rootBounds
// entry.target
// entry.time
entries.forEach( (e) => this.intersectionEvent(e) );
}
intersectionEvent(entry) {
const isIntersecting = entry.isIntersecting;
const top = entry.boundingClientRect.top;
console.log("intersectionEvent:", entry.target.innerText);
console.log("isIntersecting:", isIntersecting);
console.log("top:", top);
console.log("intersectionRect.top:", entry.intersectionRect.top);
console.log("rootBounds.top:", entry.rootBounds.top);
console.log("intersectionRect.bottom:", entry.intersectionRect.bottom);
console.log("rootBounds.bottom:", entry.rootBounds.bottom);
if (isIntersecting && Math.abs(entry.intersectionRect.top - entry.rootBounds.top) < this.distanceFromTopDetection) {
this.delinkTop(entry.target);
} else if (!isIntersecting && Math.abs(entry.intersectionRect.top - entry.rootBounds.top) < this.distanceFromTopDetection) {
this.linkTop(entry.target)
}
}
linkTop(element) {
console.log("linkTop:", element);
this.allPreviousElementsTransparent(element, this.titles);
this.allNextElementsVisible(element, this.titles);
this.elementVisible(element)
}
delinkTop(element) {
console.log("delinkTop:", element);
this.previousElementVisible(element, this.titles);
}
allPreviousElementsTransparent(element) {
console.log("allPreviousElementsTransparent:", element);
const index = this.indexElement(element);
console.log("index:", index);
// First element has not previous
if (index === 0) return;
const previousElements = this.titles.slice(0, index);
previousElements.forEach((e) => this.elementTransparent(e));
}
allNextElementsVisible(element) {
console.log("allNextElementsVisible:", element);
const index = this.indexElement(element);
// Last element has not next
if (index === this.titles.size - 1) return;
const nextElements = this.titles.slice(index + 1);
nextElements.forEach((e) => this.elementVisible(e));
}
previousElementVisible(element) {
console.log("previousElementVisible:", element);
// First element has not previous
if (this.indexElement(element) === 0) return;
const previousElement = this.titles[this.indexElement(element) - 1];
this.elementVisible(previousElement)
}
elementVisible(element) {
console.log("elementVisible:", element);
element.classList.remove("transparent");
element.classList.add("visible");
}
elementTransparent(element) {
console.log("elementTransparent:", element);
element.classList.remove("visible");
element.classList.add("transparent");
}
indexElement(element) {
const result =
this.titles.findIndex((familyElement) => {
return familyElement == element;
});
return result;
}
}
// new StickyTitles(".message.role-user", "#messages-list", 200).start();
new StickyTitles("h1", "#container", 20).start();
#container {
margin: 100px;
height: 400px;
overflow-y: scroll;
padding: 50px;
}
#container h1 {
display: inline;
background-color: darkkhaki;
}
#container h1.transparent {
transition: opacity 0.3s;
opacity: 0;
}
#container h1.visible {
transition: opacity 0.3s;
opacity: 1;
}
<html>
<head>
<title>Test</title>
</head>
<body>
<div id="container">
<h1>Title 1</h1>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum nec leo sit amet est sollicitudin viverra. Etiam
tincidunt erat et est facilisis, id laoreet sapien bibendum. Nulla rutrum odio sed est vulputate, vitae luctus massa
hendrerit. Sed sed urna venenatis sem tristique finibus. Donec tristique nisl nibh. Sed non hendrerit nulla. Nulla non
leo diam.
Nullam eu magna consequat, laoreet purus nec, luctus augue. Integer porttitor eros at tellus porta tempus. Curabitur
imperdiet odio velit, ac commodo magna tristique sed. Maecenas ornare blandit mi, in accumsan tellus aliquam vel. Sed
enim dui, tincidunt at nisi quis, accumsan tempor justo. Donec eget justo interdum, scelerisque ante nec, varius neque.
Aenean odio tortor, suscipit nec aliquam quis, viverra eget enim. In pharetra aliquam diam, sed sollicitudin elit varius
id. Morbi pulvinar tincidunt dolor.
Sed id urna eleifend, vulputate libero ac, tristique ipsum. Nullam porta erat ut tellus vestibulum ultrices. Maecenas
non est a diam eleifend dictum non vitae magna. Morbi aliquet viverra nunc et laoreet. Integer massa urna, maximus sed
mi nec, consequat consequat erat. Nulla facilisi. Nullam feugiat ligula vel elit fringilla commodo. Integer a posuere
nibh, at porttitor risus. Morbi id sem facilisis odio dapibus volutpat sit amet id leo. Nunc id volutpat urna. Nunc eget
ex vel nibh tincidunt venenatis. Proin congue quis mauris ut luctus.
</p>
<h1>Title facilisis sit amet, consectetur</h1>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum nec leo sit amet est sollicitudin viverra. Etiam
tincidunt erat et est facilisis, id laoreet sapien bibendum. Nulla rutrum odio sed est vulputate, vitae luctus massa
hendrerit. Sed sed urna venenatis sem tristique finibus. Donec tristique nisl nibh. Sed non hendrerit nulla. Nulla non
leo diam.
Nullam eu magna consequat, laoreet purus nec, luctus augue. Integer porttitor eros at tellus porta tempus. Curabitur
imperdiet odio velit, ac commodo magna tristique sed. Maecenas ornare blandit mi, in accumsan tellus aliquam vel. Sed
enim dui, tincidunt at nisi quis, accumsan tempor justo. Donec eget justo interdum, scelerisque ante nec, varius
neque.
Aenean odio tortor, suscipit nec aliquam quis, viverra eget enim. In pharetra aliquam diam, sed sollicitudin elit
varius
id. Morbi pulvinar tincidunt dolor.
Sed id urna eleifend, vulputate libero ac, tristique ipsum. Nullam porta erat ut tellus vestibulum ultrices. Maecenas
non est a diam eleifend dictum non vitae magna. Morbi aliquet viverra nunc et laoreet. Integer massa urna, maximus sed
mi nec, consequat consequat erat. Nulla facilisi. Nullam feugiat ligula vel elit fringilla commodo. Integer a posuere
nibh, at porttitor risus. Morbi id sem facilisis odio dapibus volutpat sit amet id leo. Nunc id volutpat urna. Nunc
eget
ex vel nibh tincidunt venenatis. Proin congue quis mauris ut luctus.
</p>
<h1>Title dolor sit amet</h1>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum nec leo sit amet est sollicitudin viverra. Etiam
tincidunt erat et est facilisis, id laoreet sapien bibendum. Nulla rutrum odio sed est vulputate, vitae luctus massa
hendrerit. Sed sed urna venenatis sem tristique finibus. Donec tristique nisl nibh. Sed non hendrerit nulla. Nulla non
leo diam.
Nullam eu magna consequat, laoreet purus nec, luctus augue. Integer porttitor eros at tellus porta tempus. Curabitur
imperdiet odio velit, ac commodo magna tristique sed. Maecenas ornare blandit mi, in accumsan tellus aliquam vel. Sed
enim dui, tincidunt at nisi quis, accumsan tempor justo. Donec eget justo interdum, scelerisque ante nec, varius
neque.
Aenean odio tortor, suscipit nec aliquam quis, viverra eget enim. In pharetra aliquam diam, sed sollicitudin elit
varius
id. Morbi pulvinar tincidunt dolor.
Sed id urna eleifend, vulputate libero ac, tristique ipsum. Nullam porta erat ut tellus vestibulum ultrices. Maecenas
non est a diam eleifend dictum non vitae magna. Morbi aliquet viverra nunc et laoreet. Integer massa urna, maximus sed
mi nec, consequat consequat erat. Nulla facilisi. Nullam feugiat ligula vel elit fringilla commodo. Integer a posuere
nibh, at porttitor risus. Morbi id sem facilisis odio dapibus volutpat sit amet id leo. Nunc id volutpat urna. Nunc
eget
ex vel nibh tincidunt venenatis. Proin congue quis mauris ut luctus.
</p>
<h1>Title consequat consequat erat. Nulla facilisi</h1>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum nec leo sit amet est sollicitudin viverra. Etiam
tincidunt erat et est facilisis, id laoreet sapien bibendum. Nulla rutrum odio sed est vulputate, vitae luctus massa
hendrerit. Sed sed urna venenatis sem tristique finibus. Donec tristique nisl nibh. Sed non hendrerit nulla. Nulla non
leo diam.
Nullam eu magna consequat, laoreet purus nec, luctus augue. Integer porttitor eros at tellus porta tempus. Curabitur
imperdiet odio velit, ac commodo magna tristique sed. Maecenas ornare blandit mi, in accumsan tellus aliquam vel. Sed
enim dui, tincidunt at nisi quis, accumsan tempor justo. Donec eget justo interdum, scelerisque ante nec, varius
neque.
Aenean odio tortor, suscipit nec aliquam quis, viverra eget enim. In pharetra aliquam diam, sed sollicitudin elit
varius
id. Morbi pulvinar tincidunt dolor.
Sed id urna eleifend, vulputate libero ac, tristique ipsum. Nullam porta erat ut tellus vestibulum ultrices. Maecenas
non est a diam eleifend dictum non vitae magna. Morbi aliquet viverra nunc et laoreet. Integer massa urna, maximus sed
mi nec, consequat consequat erat. Nulla facilisi. Nullam feugiat ligula vel elit fringilla commodo. Integer a posuere
nibh, at porttitor risus. Morbi id sem facilisis odio dapibus volutpat sit amet id leo. Nunc id volutpat urna. Nunc
eget
ex vel nibh tincidunt venenatis. Proin congue quis mauris ut luctus.
</p>
<h1>Title at nisi quis</h1>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum nec leo sit amet est sollicitudin viverra. Etiam
tincidunt erat et est facilisis, id laoreet sapien bibendum. Nulla rutrum odio sed est vulputate, vitae luctus massa
hendrerit. Sed sed urna venenatis sem tristique finibus. Donec tristique nisl nibh. Sed non hendrerit nulla. Nulla non
leo diam.
Nullam eu magna consequat, laoreet purus nec, luctus augue. Integer porttitor eros at tellus porta tempus. Curabitur
imperdiet odio velit, ac commodo magna tristique sed. Maecenas ornare blandit mi, in accumsan tellus aliquam vel. Sed
enim dui, tincidunt at nisi quis, accumsan tempor justo. Donec eget justo interdum, scelerisque ante nec, varius
neque.
Aenean odio tortor, suscipit nec aliquam quis, viverra eget enim. In pharetra aliquam diam, sed sollicitudin elit
varius
id. Morbi pulvinar tincidunt dolor.
Sed id urna eleifend, vulputate libero ac, tristique ipsum. Nullam porta erat ut tellus vestibulum ultrices. Maecenas
non est a diam eleifend dictum non vitae magna. Morbi aliquet viverra nunc et laoreet. Integer massa urna, maximus sed
mi nec, consequat consequat erat. Nulla facilisi. Nullam feugiat ligula vel elit fringilla commodo. Integer a posuere
nibh, at porttitor risus. Morbi id sem facilisis odio dapibus volutpat sit amet id leo. Nunc id volutpat urna. Nunc
eget
ex vel nibh tincidunt venenatis. Proin congue quis mauris ut luctus.
</p>
<h1>Title Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum nec leo sit amet est sollicitudin viverra. Etiam
tincidunt erat et est facilisis, id laoreet sapien bibendum. Nulla rutrum odio sed est vulputate, vitae luctus massa
hendrerit</h1>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum nec leo sit amet est sollicitudin viverra. Etiam
tincidunt erat et est facilisis, id laoreet sapien bibendum. Nulla rutrum odio sed est vulputate, vitae luctus massa
hendrerit. Sed sed urna venenatis sem tristique finibus. Donec tristique nisl nibh. Sed non hendrerit nulla. Nulla non
leo diam.
Nullam eu magna consequat, laoreet purus nec, luctus augue. Integer porttitor eros at tellus porta tempus. Curabitur
imperdiet odio velit, ac commodo magna tristique sed. Maecenas ornare blandit mi, in accumsan tellus aliquam vel. Sed
enim dui, tincidunt at nisi quis, accumsan tempor justo. Donec eget justo interdum, scelerisque ante nec, varius
neque.
Aenean odio tortor, suscipit nec aliquam quis, viverra eget enim. In pharetra aliquam diam, sed sollicitudin elit
varius
id. Morbi pulvinar tincidunt dolor.
Sed id urna eleifend, vulputate libero ac, tristique ipsum. Nullam porta erat ut tellus vestibulum ultrices. Maecenas
non est a diam eleifend dictum non vitae magna. Morbi aliquet viverra nunc et laoreet. Integer massa urna, maximus sed
mi nec, consequat consequat erat. Nulla facilisi. Nullam feugiat ligula vel elit fringilla commodo. Integer a posuere
nibh, at porttitor risus. Morbi id sem facilisis odio dapibus volutpat sit amet id leo. Nunc id volutpat urna. Nunc
eget
ex vel nibh tincidunt venenatis. Proin congue quis mauris ut luctus.
</p>
</div>
</body>
</html>
The main point is that all the titles are position: sticky
. This was easy. But then I had to make the previous titles invisible so they don’t get messy on the back of the actual title.
This is where things started to get complicated.
I managed to find a solution by detecting the intersection with the top or bottom of the container and making the other siblings visible or invisible.
But then I realized that if I scroll too fast back and forth, the title states start to get messy, like being invisible when they should be visible and vice versa.
I tried to minimize the bugs by making other siblings visible/invisible in many different cases to double-check that the state was right.
Still, the intersection mechanism is not very solid, and I have situations where the titles are not in the right state.
It would be easy if I had a way to:
- check all the titles that are stuck to the top (position is fixed because they have reached the top)
- make invisible all of them but the last one
I could call this method on every scroll event, and it should work. But I can’t find a way to detect what titles are actually "stuck" on the top (I can’t find an event for that). Only by intersections and, as mentioned, they are not trustable in quick scrolls.
2
Answers
From a Reddit suggested solution:
Very elegant because it doesn't require JS. But it does require to change the HTML structure from the original question.
You could use a scroll event listener and, for each header, check whether it’s bottom is touching or crossing the top of the next header, and if it is, hide it.
You can set the relative position thresholds according to how you want it to behave. Since they’re set to
position: sticky
, all "stuck" elements will have the same top position. So you could also have the prev. header disappear when its top position matches the next header’s, but then you’ll get some overlap until the next one does stick, that’s why I chose to hide them when their bottoms touch the tops of the next ones.