WebberUI

Scroll-aware Navbar

A navigation bar that condenses, hides, or widens as you scroll — three variants sharing one consistent rhythm, with nested scroll container support.

Loading preview…
npx shadcn@latest add https://webberui.com/r/scroll-aware-navbar.json

Playground

Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.

24
72
56
448
<ScrollAwareNavbar />

Installation

npx shadcn@latest add https://webberui.com/r/scroll-aware-navbar.json

Or, once registries are configured in components.json, install it as @webberui/scroll-aware-navbar.

Usage

import { ScrollAwareNavbar } from "@/components/ui/scroll-aware-navbar";

<ScrollAwareNavbar variant="shrink">
  <span className="font-semibold">Webber</span>
  <nav className="flex gap-4">
    <a href="#features">Features</a>
    <a href="#docs">Docs</a>
  </nav>
  <button className="rounded-full bg-neutral-900 px-3 py-1.5 text-white">
    Get started
  </button>
</ScrollAwareNavbar>

The navbar aligns to the top of the scroll container with sticky top-0, and children is yours to lay out (justify-between by default). If the scrolling happens inside a nested overflow container, pass that container's ref to container:

const scrollerRef = React.useRef<HTMLDivElement>(null);

<div ref={scrollerRef} className="h-[400px] overflow-y-auto">
  <ScrollAwareNavbar variant="float" container={scrollerRef}>
    {/* ... */}
  </ScrollAwareNavbar>
  {/* Content */}
</div>

Props

PropTypeDefaultDescription
childrenReact.ReactNodeNavbar content, usually the brand, links, and a call-to-action button
variant"shrink" | "hide" | "float""shrink"Scroll reaction style: condense, hide, or widen
thresholdnumber24Threshold for counting as "scrolled" (px)
expandedHeightnumber72Expanded height at the top of the page (px)
condensedHeightnumber56Condensed height after scrolling (px), used by shrink and float
pillWidthnumber448Maximum pill width for the float variant at the top of the page (px)
position"sticky" | "fixed""sticky"Positioning method; fixed means you must reserve space for the content yourself
containerRefObject<HTMLElement | null>Ref of the nested scroll container; when omitted, the whole window is tracked
classNamestringApplied to the visible navbar (the inner bar); use it to add max-w, font sizes, and so on
wrapperClassNamestringApplied to the outermost positioning container (the header), for example to override the z-index
aria-labelstring"Main navigation bar"Accessible label for the navbar landmark

The three variants

  • shrink: transparent and floating above the content at the top of the page; once you scroll past threshold, the height condenses from expandedHeight to condensedHeight and a frosted-glass background with a bottom border fades in.
  • hide: the whole bar retracts with a displacement when scrolling down and slides back when scrolling up, which lets the content area take the full height while reading. A background appears after scrolling here too.
  • float: at the top of the page it is a narrow centered pill (pillWidth) with a gap from the top edge; after scrolling it widens to fill the container, the corner radius tightens, and it aligns to the top.

How it works

  • useScroll tracks the scroll position, and the hide variant additionally compares successive displacements to determine direction; a 2px dead zone prevents jitter.
  • The float variant's target widened width is measured from the outer container's actual width with a ResizeObserver, so the widening animation is smooth and adapts to the container's size.
  • The background fades in and out as its own layer, so condensing or appearing never disturbs the content layout.

Accessibility

  • The outermost element is a <header> landmark with an aria-label; with the hide variant, the moment keyboard focus moves into a retracted navbar it slides straight back, so focus never sits off screen.
  • When the user has "reduce motion" enabled at the system level, the scroll-down displacement hiding is disabled (the navbar is always visible) and the remaining states become instant switches rather than transitions.
  • All useScroll event listeners and the ResizeObserver are fully cleaned up on unmount.

On this page