Navigation

Sticky Header

An in-flow page header that stays pinned to the top and reacts to scroll — shrinking its height, gaining a blurred glass surface, or hiding on scroll-down.

Preview in your theme

Loading preview…

"use client"

import * as React from "react"
import { cn } from "@/lib/utils"

export type StickyHeaderBehavior = "shrink" | "blur" | "hide"

export interface StickyHeaderHeights {
  /** Header height (px) while scrollTop is within the threshold band. */
  expanded: number
  /** Header height (px) once scrollTop passes the threshold (behavior="shrink" only). */
  collapsed: number
}

Installation

npx shadcn@latest add https://ui.zyeon.ai/r/sticky-header.json

Prompt

The prompt behind this component — paste it into your AI assistant to recreate or adapt it.

Build a React + TypeScript + Tailwind "StickyHeader" component (no animation
library — CSS transitions/transforms only).

Contract
- Export a forwardRef<HTMLElement, StickyHeaderProps> rendering a <header>.
- children: ReactNode — the consumer's own header content (logo, nav, actions).
  This component is a shell only; it renders nothing of its own besides children.
- behavior?: "shrink" | "blur" | "hide" (default "shrink") — which scroll-driven
  treatment to apply once scrolled past `threshold`.
- threshold?: number (default 24) — scroll distance in px past which the header
  switches from data-state="top" to data-state="scrolled".
- heights?: { expanded: number; collapsed: number } (default { expanded: 72,
  collapsed: 56 }) — heights in px used by behavior="shrink", also exposed to
  children as a --sticky-h CSS custom property regardless of behavior.
- border?: boolean (default true) — whether a border-b fades in once scrolled.
- target?: RefObject<HTMLElement | null> — scrollable container to track; omit
  to track the window (the common case — this is a real page header).
- disabled?: boolean (default false) — turns off all scroll tracking; renders
  as a plain static header, always data-state="top".
- Standard className passthrough merged via cn(), plus the rest of
  React.HTMLAttributes<HTMLElement> spread onto the root.

Behavior
- Track scroll with a ref-held "last scrollTop", not React state for the
  high-frequency math. Attach one passive scroll listener, throttled to one
  check per animation frame via requestAnimationFrame (guard re-entrancy with
  a raf id, cancel it on unmount). Kick off one throttled check immediately on
  mount (via the same rAF path, never a synchronous setState in the effect
  body) so a page/container already mid-scroll on load reports the right
  state right away.
- Listen on both the container (if target is given) and window — a target
  element may not be scrollable yet at mount time, so both stay wired for the
  component's lifetime.
- data-state: "scrolled" once scrollTop > threshold, else "top". This is the
  single source of truth every behavior reads from, and it's mirrored onto the
  root as a DOM attribute so consumers can style off it directly in CSS
  without touching JS.
- behavior="shrink": while scrolled, the header's own height eases from
  heights.expanded to heights.collapsed (set via inline style, transitioned
  via a Tailwind arbitrary transition-[height]). Regardless of behavior,
  expose the *current* effective height as a --sticky-h custom property in
  inline style, so children (a logo mark, a nav row's height) can size or
  scale themselves off the same number instead of duplicating the shrink
  math — e.g. a logo box using style={{ height: "calc(var(--sticky-h) * 0.5)" }}.
- behavior="blur": while scrolled, switch the background from transparent to
  bg-background/80 + backdrop-blur (glass-on-scroll — useful over a hero image
  that needs to show through before the header "activates").
- behavior="hide": direction-aware hide/reveal, computed only while past
  threshold (below it, always force visible, no direction math — same
  top-of-page exemption as this library's Floating Navbar). Compute
  delta = scrollTop - lastScrollTop; ignore |delta| < 8px entirely (jitter
  guard against trackpad/momentum micro-scroll, matching Floating Navbar's
  guard) — otherwise hidden = delta > 0 (down) and update the reference
  point. Hidden state renders as -translate-y-full; because the root stays
  position: sticky (not fixed), it keeps reserving its slot in the document
  flow the whole time — only its visual position slides, so the reveal reads
  as content sliding out from under a header that's still really there.
- Focus safety: focus-within:translate-y-0 always overrides the hidden
  transform, so tabbing into the header while behavior="hide" has it off
  screen brings it back rather than trapping focus somewhere invisible.
- border: while scrolled and border is true, add border-b border-border;
  otherwise (or when scrolled is false) render border-b border-transparent
  so the 1px never causes a layout jump when it appears/disappears. When
  border is false, skip the border classes entirely.
- disabled: skip the effect/listeners altogether; state stays "top", hidden
  stays false, height stays heights.expanded — a plain static header.
- Clean up: remove every scroll listener and cancel any pending
  requestAnimationFrame on unmount or when target/threshold/behavior/disabled
  change.

Rendering & styling
- Semantic tokens only: bg-background (opaque default for shrink/hide),
  bg-background/80 + backdrop-blur for behavior="blur" once scrolled,
  border-border/border-transparent for the border, no hardcoded colors.
- Root is sticky top-0 z-40 by default — an in-flow header, not a fixed
  overlay: it still reserves real layout space, which is exactly what makes
  the behavior="hide" reveal-from-underneath effect read correctly.
- transition-[height,background-color,border-color,transform] duration-300
  covers every behavior's animated property in one class list;
  motion-reduce:transition-none removes the animation only — data-state,
  data-hidden and the border/background/height values still switch instantly,
  so no behavior is lost when motion is reduced.
- Root carries data-state="top" | "scrolled" and data-hidden="true" | "false"
  as real string attributes (not React booleans, which would omit the
  attribute on false) — the whole point is consumers can write plain CSS like
  [data-state="scrolled"] & { ... } without reading component internals.
- No internal padding, height enforcement (outside shrink), or nav layout
  opinions — those live entirely in the consumer's children.

Customization levers
- Behavior: pick "shrink" for a height/logo-scale treatment, "blur" for a
  glass-on-scroll surface (great over a hero), "hide" for scroll-direction
  reveal on long content pages.
- threshold: how much top-of-page scroll stays "top" before any treatment
  kicks in and (for behavior="hide") before direction tracking starts.
- heights: tune expanded/collapsed for a subtler or more dramatic shrink; children
  reading --sticky-h automatically follow whatever values are passed.
- border: turn off entirely for a flush, borderless surface, or drop border
  and add your own conditional border via the data-state attribute selector.
- Jitter tolerance: the 8px delta guard in behavior="hide" is the debounce
  threshold — raise it for touch devices with jumpier momentum scroll.
- Composition: children own 100% of the internal layout — read --sticky-h to
  scale a logo or icon, or ignore it and just let the row's own height
  (h-full) follow the shrinking root naturally.

Concepts

  • Externalized statedata-state and data-hidden are real DOM attributes on the root, so consumers can theme or react to scroll purely in CSS ([data-state="scrolled"] &) without reading component internals or duplicating the scroll math.
  • CSS-variable height handoff--sticky-h always reflects the header's current effective height; children (a logo mark, an inner nav row) read it to scale themselves in lockstep with behavior="shrink" instead of each re-deriving the shrink math.
  • Top-of-page exemption — below threshold, the header is always in its resting state (never shrunk, never hidden) regardless of scroll direction — the same guard this library's Floating Navbar uses, reused here to gate all three behaviors from one prop.
  • Direction jitter guardbehavior="hide" ignores sub-8px scroll deltas so trackpad momentum can't flicker the header open/closed on its own.
  • In-flow, not floating — the root stays position: sticky, keeping its layout slot; that's what makes behavior="hide" read as content sliding out from under a header that's still really there, rather than a floating pill disappearing.
  • Shell, not a layout — the component owns scroll tracking, height/background/border transitions and the hide transform only; every visual choice inside the header belongs to its children.

On This Page