Tailwind CSS - Adding a Shadow to Sticky Headers on Scroll in Vue/Nuxt

December 30, 2025
8 views

Overview

A sticky header is a standard UI pattern, but it often looks "flat" when it blends into the content scrolling underneath it. The best way to solve this is to apply a shadow (or a background or both), but only once the user has started scrolling.

Implementing a "shadow on scroll" effect in Vue or Nuxt is straightforward using the Composition API and Tailwind CSS 4. Instead of monitoring the scroll position directly (which can be heavy on performance), we can use an IntersectionObserver to toggle a reactive state.

Step 1: The Template (HTML) Structure

First, we need a "sentinel" element. This is an invisible div placed at the very top of the page. When this element leaves the viewport, we know the user has scrolled and the header has become "stuck."

<template>
  <!-- The Sentinel: Invisible element at the very top -->
  <div ref="sentinel" class="absolute top-0 h-1 w-full"></div>

  <header
    :class="[
      'sticky top-0 z-50 bg-white transition-all duration-300',
      isScrolled
        ? 'border-b border-gray-200 bg-gray-100! shadow-md'
        : 'border-b-transparent shadow-none',
    ]">
    <nav class="mx-auto flex max-w-3xl items-center justify-between p-6">
      <span class="font-bold">Web Store</span>
      <ul class="flex gap-4">
        <li>Features</li>
        <li>Pricing</li>
      </ul>
    </nav>
  </header>

  <!-- Spacer for content -->
  <main class="mx-auto min-h-200 max-w-3xl p-8">
    <p>Scroll down to see the header effect...</p>
  </main>
</template>

Example showing what the header looks like before the user has scrolled.

Step 2: Vue/Nuxt Script Logic

A Vue JS example using composition API and Typescript.

<script setup lang="ts">
import { ref, onMounted, onUnmounted } from "vue";

const isScrolled = ref(false);
const sentinel = ref(null);

let observer: IntersectionObserver | null = null;

onMounted(() => {
  observer = new IntersectionObserver(
    ([entry]) => {
      // When the sentinel is out of view, we are "scrolled"
      isScrolled.value = !entry?.isIntersecting;
    },
    {
      threshold: [1.0],
    },
  );

  if (sentinel.value) {
    observer.observe(sentinel.value);
  }
});

onUnmounted(() => {
  if (observer) observer.disconnect();
});
</script>

Example showing what the header looks after the user has scrolled.

Notes on Tailwind Classes

  • sticky top-0: Keeps the header at the top of the page.
  • transition-all duration-300: In Tailwind 4, this ensures the shadow and background fades in and out smoothly rather than snapping instantly.
  • z-50: In Tailwind 4, this is the z-index. Ensures that header section sits above content being scrolled below.
  • bg-gray-100!: In Tailwind 4, ! is the equivalent to CSS !important. Ensures that the background color specified here takes precedence.