12 hrs left sale ends soon
Free shipping on everything · ends Sunday
@magic-spells/sticky-header

Follows your scroll

The header translates at exactly the rate you scroll — no easing, no lag, no trigger points. When scrolling stops it settles to fully shown or fully hidden, whichever side of the threshold it landed on.

npm install @magic-spells/sticky-header

6.1 kB JS gzip · 0.3 kB CSS gzip

Rider A · top = group height ·

Everything you need

<!-- or from npm: import '@magic-spells/sticky-header' + '/css' -->
<link rel="stylesheet"
      href="https://unpkg.com/@magic-spells/sticky-header/dist/sticky-header.min.css">
<script src="https://unpkg.com/@magic-spells/sticky-header"></script>

<style>
  /* the bounce cap inherits this, so it belongs on the host */
  sticky-header { background: #0f0e14; }
</style>

<sticky-header hide-on-scroll="both" breakpoint="1024" hover-lock>
  <!-- no tag: top-only. Leaves with the page, back near the top -->
  <div class="countdown">12 hrs left</div>

  <!-- tagged for mobile: comes back on a scroll up, small screens only -->
  <div data-announcement data-sticky-reveal="mobile">Free shipping</div>

  <!-- bare tag = both. The reveal boundary on desktop -->
  <header data-sticky-reveal>
    <a href="/">Brand</a>
    <nav>…</nav>
  </header>
</sticky-header>

<sticky-content>
  <nav class="section-tabs">…</nav>  <!-- pins below, rides the offset -->
</sticky-content>

API

<sticky-header> attributes

Attribute Values Default Description
hide-on-scroll none mobile desktop both both Which viewports hide on scroll. Height variables are maintained in every mode
breakpoint px number 1024 The desktop cutoff. desktop matches (min-width: bp); mobile is everything narrower
reveal-threshold px 100 Within this distance of the page top the header is always fully visible
settle-threshold 01 0.5 How far into the gap between the two adjacent stops an idle settle commits to the hidden one rather than back to the reveal stop
settle-duration ms 900 Hide settle duration; the show settle uses 85% of it
settle-overshoot 00.2 0.05 Bounce amplitude. 0 swaps to a critically damped curve — no bounce
hover-lock boolean While the pointer is anywhere in the header group, don't hide on scroll
lock CSS selector Extra force-show condition, e.g. an open menu panel. dialog[open] is built in
locked boolean Force fully visible
disabled boolean Turn tracking off entirely (height variables are still maintained)

Descendant attributes — the reveal boundary

Attribute Values On Description
data-sticky-reveal both mobile desktop none, or bare Any descendant Comes back on a mid-page scroll up. The topmost active tag is the boundary; everything above it is top-only. Bare = both; none and unrecognized values are inactive; an element with no box is skipped

<sticky-content> attributes

Attribute Values Default Description
top CSS length --header-group-height Resting sticky inset. Sugar for setting --sticky-content-top
disabled boolean Stop riding the header; pin at the static resting inset instead. stuck is no longer applied

Custom properties — written on <body>

Property Description
--header-group-offset The rider hook. Per frame, 0 → −(group height); briefly a little past 0 during the show bounce
--header-group-height Total height of the sliding group
--header-height The header alone, without the announcement bar
--announcement-bar-height The data-announcement child, when there is one
--header-reveal-offset The middle resting stop — −(distance from the group top to the reveal boundary). 0px with nothing tagged

Custom properties — set by you

Property Default Description
--sticky-content-top --header-group-height A rider's resting inset
--sticky-header-z-index 100 Header stacking order
--sticky-content-z-index 10 Rider stacking order

State attributes

Attribute On When
data-state="top|scrolling" <body> Within 8px of the page top, or not
data-header-hidden <body> Settled fully hidden — never mid-transition
data-header-revealed <body> Settled at the reveal stop, with a boundary strictly inside the group
data-header-tracking <body> Tracking or settling. Use it to suppress your own transitions
data-header-locked <body> A lock condition is holding the header visible
data-hidden data-tracking <sticky-header> Element-level mirrors of the above
stuck <sticky-content> The rider is pinned at its effective top — a styling hook

Events — bubble, dispatched on <sticky-header>

Event When detail
sticky-header:settle A settle tween starts { target: 'show'|'hide', from, duration }
sticky-header:hide Settled fully hidden
sticky-header:show Settled anywhere short of fully hidden — at a partial reveal stop this fires alongside reveal
sticky-header:reveal Settled at the reveal stop
sticky-header:resize Measured geometry changed { headerHeight, announcementHeight, groupHeight }

Properties and methods

Member On Description
offset <sticky-header> Current offset in px (0 shown → −groupHeight hidden)
progress <sticky-header> Hidden fraction over the full travel, 01
revealOffset <sticky-header> The reveal stop in px, 0 with nothing tagged
isHidden <sticky-header> Settled fully hidden. Named to avoid shadowing native HTMLElement.hidden
groupHeight <sticky-header> Measured height of the sliding group
show() hide() <sticky-header> Settle fully visible / fully hidden (hide is ignored while locked or inactive)
lock() unlock() <sticky-header> Toggle the locked attribute
refresh() <sticky-header> Re-measure geometry and rebase scroll tracking
stuck <sticky-content> Whether the rider is pinned at its effective top
top <sticky-content> Resting inset in px, excluding the header offset
Rider B · top = group + 5rem ·
Nav panel open — header locked visible

Menu

While this dialog is open the header is locked fully visible. Scroll the page behind it — the header will not move. Close it and normal tracking resumes from the current position.