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.
Loading preview…
Installation
npx shadcn@latest add https://ui.zyeon.ai/r/sticky-header.jsonPrompt
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 state —
data-stateanddata-hiddenare 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-halways reflects the header's current effective height; children (a logo mark, an inner nav row) read it to scale themselves in lockstep withbehavior="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 guard —
behavior="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 makesbehavior="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.
Command Palette
A ⌘K-style command palette — portal dialog, fuzzy subsequence search, grouped commands, recent items and full keyboard navigation.
Mega Menu
A site navigation bar whose entries drop a wide multi-column sheet across the whole navbar — portalled past clipping ancestors, opened with hover intent, and folded into an accordion when it gets narrow.