CSS position:sticky – How to detect element is stuck?

2 August 2022

position:sticky and issue we have with it

I don’t really like long article intros if the issue is pretty clear from the article’s title.

So just in a few words: we still have no pseudo class to detect is position:sticky element is stuck at the moment or stay in its initial place. Some unresolved limitations don’t allow to create an universal solution for everyone.

Until it’s done, we can use only JS solutions to achieve similar behavior. It’s not perfect cause there are might be a lot of issues with JS so don’t rely on it a lot and don’t create something significant using it.

In my case I’d want only to change a pinch of styles for a stuck element. Nothing criminal.

After googling this question I found the most popular solution is to use IntersectionObserver entity to detect where is the sticky element relatively its parent:

const observer = new IntersectionObserver( 
  ([e]) => e.target.toggleAttribute('stuck', e.intersectionRatio < 1),
  {threshold: [1]}
);

observer.observe(document.querySelector('nav'));

Problem

Yes, yes, well done, Slytherin! HOWEVER:

It really works, but all of this code examples are made for the top-sticky elements. Then the element is placed at the bottom of the container and has bottom:0 (or more than zero) property, it will be stuck to the bottom of the container.

And that was my case. Namely for one of my plugins called WooCommerce Products Wizard.

Here is it’s demo page to see it in action: https://products-wizard.troll-winner.com/

There is a section with top and bottom control bars. And the bottom one is using position:sticky property.

Here how it looks at the very bottom of the section:

Sticky element at the bottom of the container

The thing I’d want to do is to add a small box-shadow of the control bar when it’s not at the very bottom of the container. Like this:

Stuck element with shadow

Solution

So then we have a question, let’s look for an answer!

Let’s start with the minimal markup and styles.

<div class="sticky-bottom" data-component="sticky-observer">I'm sticky bottom!</div>
.sticky-bottom {
    position: sticky;
    bottom: 0;
}

Then we need to collect and prepare our elements for work. We’ll do this at the window.load event to avoid different side effects and laayout shifts.

On the page load event our sticky element already can be stuck somewhere in a middle of the container.

So how can we check the element is stuck? I haven’t found an easy answer. This implementation is pretty deep in the browser’s entrails with no handlers or clues. At least I can’t find it. Give me a sign if you know a better way.

Here is a harsh solution I found:

We go through our elements and store current element position relative to its parent into a variable using the getBoundingClientRect method. Then we apply position:static property to make element not sticky for an instance.

After that we can compare the previous top value with the current one to define is the element already stuck in the container. If they aren’t equal that meants element is currently stuck. Finally, we need to reset the position property and save the variable value as a data-attribute to work with it later:

let stickyObserverElements = null;

function updateStickyObserverElementsState() {
    // if elements are not ready yet
    if (!stickyObserverElements) {
        return;
    }

    stickyObserverElements.forEach(function (element) {
        const parentTop = element.parentElement.getBoundingClientRect().top;
        const top = Math.round(element.getBoundingClientRect().top - parentTop);

        element.style.position = 'static';
        element.toggleAttribute(
            'stuck',
            top !== Math.round(element.getBoundingClientRect().top - parentTop)
        );
        
        element.style.position = '';
        element.dataset.prevClientTop = String(top);
    });
}

window.addEventListener('load', () => {
    stickyObserverElements = document.querySelectorAll('[data-component~="sticky-observer"]');

    updateStickyObserverElementsState();
});

We need to use the same way to check elements position while window resizing event, so we’ll add an event listener for this.

And here is the final trick: using the window scroll event we’ll compare the previous bounding top position value with the current one relative to the parent’s position. If they aren’t equal, that means the element is currently stuck.

window.addEventListener('resize', () => updateStickyObserverElementsState());

window.addEventListener('scroll', () => {
    // if elements are not ready yet
    if (!stickyObserverElements) {
        return;
    }

    stickyObserverElements.forEach(function (element) {
        const top = Math.round(element.getBoundingClientRect().top - element.parentElement.getBoundingClientRect().top);

        element.toggleAttribute('stuck', Number(element.dataset.prevClientTop) !== top);
        element.dataset.prevClientTop = String(top);
    });
});

Demo

Pretty tricky, isn’t it? Let’s finally look at the top and bottom sticky elements examples:

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam vitae massa eu est euismod dapibus eget id lectus. Donec condimentum eget magna eget suscipit. Ut nec eros porta, hendrerit erat a, posuere ligula. Curabitur pharetra mauris ligula, in laoreet nisl rhoncus vitae. Ut vehicula nisl sed interdum imperdiet. Mauris vitae nulla quam. Integer elementum elit nec varius pharetra. Maecenas ultrices id tellus sit amet sagittis. Mauris facilisis, nibh id tristique facilisis, justo turpis ornare sem, ut volutpat ligula augue nec augue. Vivamus et elementum ipsum, a blandit felis.

Mauris sed ipsum sed diam tincidunt hendrerit. Vestibulum interdum elit nec mi auctor, ut dapibus mauris tempus. Aenean non tortor ac arcu convallis facilisis nec eget lectus. Morbi efficitur leo ut dolor facilisis, tempus ultrices ex pellentesque. Aliquam tincidunt, urna rutrum varius lobortis, diam magna ultricies erat, sed viverra felis libero vel erat. Proin a ornare orci, in posuere leo. Quisque quis neque leo. Morbi iaculis est vel est tincidunt iaculis.

I’m sticky bottom!

I’m sticky top!

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam vitae massa eu est euismod dapibus eget id lectus. Donec condimentum eget magna eget suscipit. Ut nec eros porta, hendrerit erat a, posuere ligula. Curabitur pharetra mauris ligula, in laoreet nisl rhoncus vitae. Ut vehicula nisl sed interdum imperdiet. Mauris vitae nulla quam. Integer elementum elit nec varius pharetra. Maecenas ultrices id tellus sit amet sagittis. Mauris facilisis, nibh id tristique facilisis, justo turpis ornare sem, ut volutpat ligula augue nec augue. Vivamus et elementum ipsum, a blandit felis.

Mauris sed ipsum sed diam tincidunt hendrerit. Vestibulum interdum elit nec mi auctor, ut dapibus mauris tempus. Aenean non tortor ac arcu convallis facilisis nec eget lectus. Morbi efficitur leo ut dolor facilisis, tempus ultrices ex pellentesque. Aliquam tincidunt, urna rutrum varius lobortis, diam magna ultricies erat, sed viverra felis libero vel erat. Proin a ornare orci, in posuere leo. Quisque quis neque leo. Morbi iaculis est vel est tincidunt iaculis.

The same JS code is used with this small CSS to make it a bit more obvious.

.sticky-parent {
    border: 1px solid #ccc;
    padding: 1rem;
}

.sticky-top,
.sticky-bottom {
    position: sticky;
    z-index: 1;
    background-color: lightseagreen;
    color: #fff;
    padding: 1rem;
    text-align: center;
    transition: background-color 0.15s linear;
}

.sticky-top {
    top: 5rem;
}

.sticky-bottom {
    bottom: 1rem;
}

.sticky-top[stuck],
.sticky-bottom[stuck] {
    background-color: tomato;
}

What issues we have at the moment:

  • The “stuck” attribute isn’t applied while the sticky element is at the edge positions;
  • The script doesn’t work for containers with inner scroll – only for the whole page scrolling.

It’s not critical for me right now, so maybe I’ll find a solution lately. Feel free to message me if you know how to solve it 😉

Thank you for reading! I hope you like it. At least I like 😀 Happy coding!

3