Skip to content
GitHub Twitter

Change Sticky Header Color

Introduction

Have you ever seen one of those impressive sticky headers that have a base color until they become "sticky", at which point they change color to aid visibility?

Inspiration

The initial idea for this post came from Spotify's table, which has a header with a transparent background which fades to gray once it becomes pinned to the top.

I don't have any footage of it, but check the video below to get a better idea of what I mean.

For the solution we will be using the Intersection Observer API to track when the table header intersects with the top of the viewport.

The inspiration for the solution came from here. I just converted it to a React hook to consume in a Next app and added some Tailwind classes to pretty it up a bit.

Setup

For this demo we will be using shadcn/ui, which has a great-looking Data Table component.

The setup is pretty straightforward. Run through the install instructions. Then in the root folder of your app run the following commands to install the required dependencies for the Data Table component:

pnpx shadcn-ui add table
pnpm add @tanstack/react-table

This will install some basic components into the @/components/ui/table directory, such as TableHead and TableBody. These components are used to build the full table.

We import these component in our complete table component, Table, as per the shadcn/ui docs.

Intersection Observer Hook

As stated above, we essentially want to change the color of the table's header when it intersects with the top of the page. We do this by creating a useIntersectionObserver hook which returns a ref to the table header. When the header intersects with the top of the page, we toggle an is-pinned class to the header.

import { useEffect, useRef } from "react";

export const useIntersectionObserver = () => {
  const ref = useRef(null);

  useEffect(() => {
    const node = ref?.current;

    if (!node || typeof IntersectionObserver !== "function") {
      return;
    }

    const observer = new IntersectionObserver(
      ([entry]) => {
        console.log(entry);
        if (entry)
          entry.target.classList.toggle(
            "is-pinned",
            entry.intersectionRatio < 1
          );
      },
      { threshold: [1], rootMargin: "10px" }
    );

    observer.observe(node);

    return () => {
      observer.disconnect();
    };
  }, []);

  return [ref];
};

Add ref to Table header

We now move back to the Table component and add the ref to the TableHeader component, also adding a few Tailwind classes to make the header sticky and to transition the color change.

<TableHeader ref={ref} className="sticky -top-[1px] transition-colors">

Add CSS

Finally, we add the CSS to change the color of the header when it becomes pinned to the top of the page.

.is-pinned {
  @apply bg-gray-800 shadow-white;
}

Conclusion

And that's it! We now have a table header that changes color when it becomes pinned to the top of the page. You can see the full code here