WebberUI

Strata Headers

Section headers pin to the top and stack up in order once you scroll past them, accumulating into a live table of contents built out of your reading progress.

Loading preview…
npx shadcn@latest add https://webberui.com/r/strata-headers.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.

40
<StrataHeaders />

Installation

npx shadcn@latest add https://webberui.com/r/strata-headers.json

Usage

Wrap each section in a StrataSection and its header row stacks at the top automatically once you scroll past it; clicking any header row scrolls back to that section:

import { StrataHeaders, StrataSection } from "@/components/ui/strata-headers";

<StrataHeaders headerHeight={40}>
  <StrataSection title="Chapter one: Origins">
    <p>Section content…</p>
  </StrataSection>
  <StrataSection title="Chapter two: Growth">
    <p>Section content…</p>
  </StrataSection>
  <StrataSection title="Chapter three: Today">
    <p>Section content…</p>
  </StrataSection>
</StrataHeaders>

The scroll container can be the page itself or any block with overflow-y: auto (like the demo above) — the component detects the nearest scrollable ancestor automatically, so there is no container ref to pass in.

Props

StrataHeaders

PropTypeDefaultDescription
childrenReact.ReactNodePut StrataSection children directly inside; each child is one section
headerHeightnumber40Height of each header row (px), which also sets the spacing between stacked layers
classNamestringClass appended to the outermost container

StrataSection

PropTypeDefaultDescription
titleReact.ReactNodeSection title, shown on the stackable header row
childrenReact.ReactNodeSection content
idstringAnchor id applied to the header row
classNamestringClass appended to the header row button (for example, to override the default bg-white dark:bg-neutral-950 to match your page background)

How it works

  • The stacking is done natively in CSS: header row i gets position: sticky; top: i * headerHeight, so the layout is not driven by scroll events and scrolling performs exactly as it does natively
  • Pinning detection uses IntersectionObserver sentinels — each section places a 1px in-flow sentinel before its header row, and the pinned state flips as soon as the sentinel is scrolled past the pinning line, which costs almost nothing
  • When pinned, the type size shrinks (a transform scale, which does not trigger layout), an underline and a shadow appear, and the section number marks its place in the stack
  • Clicking any header row (pinned or not) smoothly scrolls back to the top of that section, with the scroll target already offset by the height of the stack above, so the start of the section is never covered
  • When the user has "reduce motion" enabled at the system level, the sticky stacking is kept because it is layout rather than motion, but the scale / shadow transitions and the smooth scrolling are both disabled (jumping straight into position)
  • The header row is a native <button>: keyboard focusable, activated with Enter, and the pinned section is marked aria-current

On this page