DEV Community

Michael Musatov
Michael Musatov

Posted on

Sticky header with dynamic shadow using tailwind

Implementing sticky header with shadow

Recently, I had fun experimenting with styles by implementing a sticky header. Making a header sticky is fairly straightforward, but adding a shadow that only appears when the header becomes sticky introduces a delightful challenge. This makes the task much more interesting and non-trivial.

Creating the Boilerplate Project

For this project, I'm opting to use NextJS with pnpm, which is a fast and disk space-efficient package manager. You can read more about why one might choose pnpm over other package managers here.

To get started, create a new NextJS project by running:

pnpm create next-app
Enter fullscreen mode Exit fullscreen mode

After setting up the project, remove any unused code to keep things tidy. Next, prepare a card component that will help emulate scrollable content. This component will serve as a practical demonstration of the sticky header as you scroll. All the changes can be found in the repository.

Implementing the Header

Creating a sticky header in Tailwind CSS is quite straightforward. Let's encapsulate this functionality within a dedicated component:

export const StickyHeader: FC<{ title: string }> = ({ title }) => (
  <h1 className="bg-slate-100 sticky top-0 w-full p-4 text-center">{title}</h1>
);
Enter fullscreen mode Exit fullscreen mode

This component uses Tailwind CSS classes to make the header sticky, ensuring it stays at the top as you scroll by applying top-0.

Without shadow
It works. However, an issue arises where cards beneath the header seem to disappear abruptly as they pass under it. To address this visual glitch, it would be beneficial to add a border or shadow at the bottom of the sticky header to delineate it from the content below once it becomes fixed to the top. This adjustment will lead us into the more complex aspects of our implementation.

There is no straightforward CSS-only way to detect when the header becomes sticky, but we can use a small hack to achieve desired behavior.

Lets start from adding the shadow.

export const StickyHeader: FC<{ title: string }> = ({ title }) => (
  <h1 className="bg-slate-100 sticky top-0 w-full p-4 text-center text-lg shadow">
    {title}
  </h1>
);
Enter fullscreen mode Exit fullscreen mode

And here is the result:
With shadow around

A shadow added around the entire header using Tailwind CSS's shadow class. Initially, this seems like a solution, but it introduces new problems, as you can see in the animation above:

  • The shadow appears around the entire header, not just at the bottom.
  • The shadow is always visible, not only when the header is sticky.

Let’s address these issues one by one to refine our header's behavior.

Shadow Only to the Bottom

Tailwind CSS doesn’t provide predefined classes for applying shadows to specific sides of an element. However, we can use arbitrary values within Tailwind classes, custom css classes or extend Tailwind's theme to include custom styles. For production environments, extending the theme is often preferred. This approach takes advantage of Tailwind’s Just-in-Time (JIT) compiler, which generates CSS only for the classes used, optimizing performance and keeps them reusable unlike usage arbitrary values.

Here's how you can extend the theme to include a custom class shadow-b for bottom-only shadows:

const config: Config = {
    ...
    theme: {
        ...
        boxShadow: {
            ...
            b: "0 5px 8px -1px rgba(0, 0, 0, 0.1)",
            ...
        },
    },
    ...
};
Enter fullscreen mode Exit fullscreen mode

Apply this new shadow style to our header (Shadow changed from shadow to shadow-b):

export const StickyHeader: FC<{ title: string }> = ({ title }) => (
  <h1 className="bg-slate-100 shadow-b sticky top-0 w-full p-4 text-center text-lg">
    {title}
  </h1>
);
Enter fullscreen mode Exit fullscreen mode

Checking the result:
With shadow below

After applying the custom shadow, the visual results show the shadow only at the bottom of the header, aligning more closely with our design goals. However, the shadow is still visible from the start, not just when the header is sticky. Our next step is to tackle this issue, ensuring the shadow appears only when needed.

Hide the Shadow and Display When Sticks

To control the visibility of the shadow only when the header is sticky, we can employ a clever CSS trick: overlaying an invisible element with a higher z-index right above the shadow. This method effectively hides the shadow under normal circumstances and only reveals it when the header sticks to the top.

Here's how we implement it:

export const StickyHeader: FC<{ title: string }> = ({ title }) => (
  <>
    <h1 className="bg-slate-100 shadow-b sticky top-0 w-full p-4 text-center text-lg">
      {title}
    </h1>
    <div className="bg-slate-100 z-10 -mt-4 h-2 w-full" />
  </>
);
Enter fullscreen mode Exit fullscreen mode

The div is placed to overlap the shadow using negative margin (-mt-4). This setup ensures that the shadow is invisible until the header becomes sticky.

After implementing this method, the shadow behaves as intended.
Sticks with shadow below
However, a nasty visual artifact arises: the element used to hide the shadow partially overlaps the header's text during scrolling when the header is sticky, as shown in the animation.
Sticks with shadow below issue
To address this new challenge, we need to refine our approach so that the text remains unobstructed while maintaining the shadow effect.

Fixing UI bug

To resolve the overlapping issue, we adjusted the structure and z-index of our elements. By separating the shadow from the header and setting proper z-index values, we can keep the shadow visible only when needed, without overlapping element interfering with the header text.

Here’s the updated component:

export const StickyHeader: FC<{ title: string }> = ({ title }) => (
  <>
    <h1 className="bg-slate-100 sticky top-0 -mb-4 w-full p-4 text-center text-lg">
      {title}
    </h1>
    <div className="shadow-b sticky top-[52px] h-2 w-full" />
    <div className="bg-slate-100 z-10 -mt-4 h-2 w-full" />
  </>
);
Enter fullscreen mode Exit fullscreen mode

In this setup:

  • The -mb-4 margin adjustment on the header compensates for the space taken by additional elements.
  • The shadow is now on a separate div that is positioned just below the header. The sticky top-[52px] positioning ensures it remains in place as you scroll.
  • A third div, also is not sticky but with higher z-index than shadow, so it covers the shadow when the header is not at the top, preventing it from showing prematurely.

With these changes, the shadow now appears correctly only when the header sticks to the top, and no part of the header text is obscured during scrolling. Here’s how it looks in action:
Final result
This final adjustment effectively resolves the visual glitch and achieves the desired effect with a clean, functional UI.

Summary

In this tutorial, we detailed how to implement a sticky header with a dynamic shadow using Tailwind CSS. The solution ensures a seamless shadow appearance as the header transitions to a fixed position. It worked for me for both web and hybrid applications. Though I will be happy to know about other approaches.

Top comments (0)