People working on laptops

Stencil Sticky Container With Scroll Shadow

by Nikita Verkhoshintcev

I remember the days when, to implement the infinite scroll or a scroll shadow, you would've used position fixed or absolute, listened for the scroll event, and detected if it reached the end by comparing the scroll position and container boundaries.

On scroll, events fire often, so you should not have complex operations such as render in the callback because it leads to poor performance.

Nowadays, there is a more reliable and easy way to achieve it. Thanks to the Intersection Observer API.

In this post, I want to show how you can create a utility sticky container with a scroll shadow using that API.

I will use Stencil as an example, but it doesn't matter what framework to use. The concept is the same.

Design

As an architecture, we will create a higher-order component that acts as a container.

The end goal is to put any other element inside. The browser will render it sticky to the bottom and add a scroll shadow if any content is beyond.

So the API should look like:

<sticky-container>
  <!-- any element that should be sticky -->
</sticky-container>

We will combine position sticky with the Intersection Observer to detect whether it should have stuck styles.

Position sticky is a great CSS rule because it handles most of the functionality out of the box. It does what it should do precisely, i.e., sticks the element to the bottom and displays it inline once visible in the viewport.

One thing that we need to add is the scroll shadow styles.

We will use the combination of bottom position and intersection observer to identify when the element fully intersects with its container.

We use a negative bottom position with threshold 1 to identify whether it fully intersects.

Note: A significant improvement is that the intersection observer triggers callback only when it changes.

Threshold 1 considers the element intersecting if 100% of its space intersects.

In practice, it means that we need to reverse the logic.

Because of this 1px gap, if the sticky container fully intersects, there is content below. Thus, we display the scroll shadow styles.

Otherwise, once we reach the end, it will intersect 100% - 1px, meaning the browser doesn't consider it intersecting because we require 100%.

That means we can apply conditional styles if intersectionRatio < 1 or !isIntersecting.

Note: You can specify the root element that you observe. The default target is the parent element.

We observe the host element intersection with the parent container on the component render.

If it is not intersecting, we modify the styles to indicate there is content beyond.

We render children via slots.

Finally, we will stop the observer in the web component disconnected callback.

Implementation

Here is an example of the implementation of the sticky container.

import { Component, Host, h, State } from "@stencil/core";

@Component({
  tag: "sticky-container",
  styleUrl: "sticky-container.scss",
})
export class StickyContainer {
  @State() stuck = false;

  private containerElement?: HTMLElement;
  private observer: IntersectionObserver;

  componentDidLoad() {
    this.observer = new IntersectionObserver(
      ([e]) => {
        this.stuck = e.intersectionRatio < 1;
      },
      {
        threshold: 1,
      }
    );

    if (this.containerElement) {
      this.observer.observe(this.containerElement);
    }
  }

  disconnectedCallback() {
    if (this.containerElement) {
      this.observer.unobserve(this.containerElement);
    }
  }

  render() {
    return (
      <Host
        ref={(el) => (this.containerElement = el)}
        class={{
          "sticky-container": true,
          "sticky-container--stuck": this.stuck,
        }}
      >
        <slot />
      </Host>
    );
  }
}

We need to specify the position and set the display for the styles of the host element.

Set the negative 1px bottom position and compensate it with the 1px padding.

Note: You cannot set a margin for sticky elements.

For the shadow scroll styles, let's add a shadow.

Here is an example of the component styles.

.sticky-container {
  position: sticky;
  display: block;
  bottom: -1px; // Required for the intersection observer to work
  padding-bottom: 1px;

  &--stuck {
    box-shadow: 0 -8px 20px rgba(0, 0, 0, 0.1);
  }
}

Here is an example of the usage.

<sticky-container>
  <div>
    <label> <input type="checkbox" /> Accept terms and conditions </label>
  </div>
</sticky-container>

Conclusion

In this post, I provided an example of how you can use the Intersection Observer API to implement scroll shadow reliably and with ease.

There are many use cases for the intersection observer, and this is just one of them.

You can stick elements to any side of the screen, which might be helpful, for example, if you work with large data tables.

Implement infinite scrolling or lazily load the content.

Here, you can find a StackBlitz demo with the final implementation and see it in action.

Nikita Verkhoshintcev photo

Nikita Verkhoshintcev

Salesforce Freelance Developer / Solution Architect

I'm a senior freelance Salesforce and full-stack web developer based in Helsinki, Finland. I help companies, consulting agencies, and ISV partners build custom Salesforce applications.

Let's work together!

We help Salesforce customers and SI/ISV partners build custom Salesforce applications. Let's discuss how we can help you!

Contact us