Implementing a Sticky Titles feature

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.

Any ideas or suggestions would be very appreciate, thanks!