Skip to content

nav

aa-nav is a small cluster of behaviours bundled around a single nav element. Pick the ones you need by composing space-separated tokens on the aa-nav attribute and adding optional sub-attributes to children of the nav.

aa-nav tokenEffect
(empty)No hide, no class. Other features (is-current tracking, indicators, section classes) still work.
hideCSS-translate the nav up off-screen when the user scrolls down (only past the global ~50px scroll-started threshold), slides it back in when they scroll up.
changeAdd is-scrolled to the nav once scrollY > 100. Useful for shrinking padding or changing background.
change-150Same, threshold 150px (any pixel value).
hide changeBoth. Tokens are space-separated and order-independent.
noneDon’t init at this breakpoint. Combine with | or -sm / -md / -lg / -xl suffixes.

The hide animation is driven by CSS, not GSAP. The library reads your aa-distance / aa-duration / aa-ease attributes and writes them onto the nav element as CSS variables — --aa-nav-hide-y (the precomputed off-screen translateY percentage), --aa-nav-duration, --aa-nav-ease. A single keyframe animation scoped on body[aa-scroll-direction="down"][aa-scroll-started="true"] then drives the nav. Cheap, no scroll-event listener, no GSAP cycle.

The hide uses CSS animation (not transition) on purpose: that way you can freely add transition: padding 0.3s, background 0.3s on .is-scrolled (or any other state class on the nav) without worrying about the transition shorthand clobbering the hide. Animation and transition are independent shorthands.

The hide threshold is shared, not per-nav: the body picks up aa-scroll-started="true" once window.scrollY > 50 (set by the body scroll-state tracker) and the hide rule is gated on that. This keeps the nav put while you’re at the top of the page so a small mouse-wheel nudge downward doesn’t immediately hide it. The change token, by contrast, is per-nav and configurable via change-<px> because it just toggles a class on its own element.

AttributeDefaultNotes
aa-distance1.5Multiplier on the -100% translate. 1 = the full nav height; 1.5 (default) clears most drop-shadows; 2 slides further still.
aa-duration(init duration)CSS transition duration in seconds.
aa-easeosmoNav-specific default — the hide animation runs via CSS keyframes, so the ease must be CSS-expressible. The init({ ease }) default (power4.out) is GSAP-only and doesn’t apply here. Accepts any v8 named ease (osmo, energy, smooth, punch, relaxed, jump, pop, anticipate, fade), a cubic-bezier(...), or a CSS keyword (linear, ease, ease-in-out, …).

elastic and bounce aren’t expressible as a single cubic-bezier; if you pass one of those, the nav falls back to osmo and a debug warning is logged. Use a GSAP-driven approach if you really need them — but they’re rarely the right call for a sticky nav anyway.

Any [aa-scroll-target] link inside the nav whose target selector resolves to a section automatically picks up an is-current class while that section is the one closest to the viewport’s middle. ScrollTrigger watches each section between 0% 50% and 100% 50%. Style off [aa-scroll-target].is-current for active-state styling.

This is independent of the click-to-scroll behaviour described in the scroll-target section below — both share the same attribute.

Add an empty element inside the nav with one of these attributes and the lib will Flip-morph it to track the active or hovered link:

AttributeBehaviour
aa-nav-current-indicatorMorphs onto whichever [aa-scroll-target] has is-current. Updates via MutationObserver and on resize.
aa-nav-hover-indicatorMorphs onto the hovered link, returns to the current link on mouseleave of the nav. Disabled on touch devices.

Both read aa-duration (default 0.4s) and aa-ease (default power2.out) from the indicator element itself. Indicator easing is GSAP-driven (it animates via Flip.fit), so the full GSAP ease vocabulary is available — including elastic.out and bounce.out.

The lib starts indicators at opacity: 0 until the first Flip lands; set the initial style yourself if you want a different fade-in.

Add aa-nav-section="my-class" to a section anywhere on the page. While that section is in view, my-class is on the nav element. Useful for swapping the nav’s colour scheme per section (light header over a hero, dark over a content panel).

<section aa-nav-section="is-light" aa-scroll-start="top 0%" aa-scroll-end="bottom 0%"></section>
<section aa-nav-section="is-dark" ></section>

aa-scroll-start / aa-scroll-end on the section override the default ScrollTrigger window of top 0% / bottom 0%.

A small global utility (not nav-specific): clicking any [aa-scroll-target="#selector"] element smooth-scrolls to that selector. If Lenis is loaded (the default), the scroll runs through lenis.scrollTo with a quartic ease and your aa-duration. If Lenis isn’t active, the lib falls back to native window.scrollTo({ behavior: 'smooth' }) — same aa-distance offset, no library configuration required.

AttributeDefaultNotes
aa-duration1.2Lenis scroll duration in seconds. Ignored on the native fallback (browser controls duration).
aa-distance0Pixel offset applied to the target’s top — negative pulls the target above the viewport edge.

Two attributes the lib writes on <body> that any CSS rule can hook into. Always-on; opt out with init({ scrollState: false }).

Body attributeValuesWhen
aa-scroll-directionup / downFlips when scroll movement exceeds 5px in the new direction.
aa-scroll-startedtrue / falseFlips to true once scrollY > 50. Used by the nav-hide CSS rule to suppress the hide animation while at the top of the page.
body[aa-scroll-direction="down"] .my-floating-button { opacity: 0; pointer-events: none; }
body[aa-scroll-started="true"] .hero { background: var(--page-bg-pinned); }

ScrollTrigger for is-current tracking and section classes, Flip for the indicators. The hide / change / scroll-target / scroll-state behaviours all run without any GSAP plugin (CSS variables + plain scroll listeners). If Flip is missing, indicators silently no-op.

Live demo — sticky pill nav with hover + current indicators and section colour swap

Section titled “Live demo — sticky pill nav with hover + current indicators and section colour swap”

A persistent (non-hiding) sticky nav. The current-link indicator (lime) sits behind the active link permanently; the hover indicator (accent-colour border, no fill) slides over whichever link the cursor is on and returns to the current link on mouseleave. While the third section is in view, aa-nav-section="is-accent" flips the nav into its accent colour scheme.

aa-nav (no token — no hide, no is-scrolled). Indicators carry aa-duration="0.45" + aa-ease="power3.out" (current) and aa-duration="0.35" + aa-ease="power2.out" (hover). Both pills get border-radius: 999px via CSS so Flip morphs between rounded shapes.

overview

The current-link indicator (lime pill) follows whichever section’s middle is in the viewport. Click any link to scroll to that section.

approach

Hover any link and the accent-bordered pill slides over it via Flip. Move the cursor away — it returns to the current pill.

work

This section adds is-accent to the nav. The whole bar inverts; the current pill flips to white so it stays readable on the dark background.

contact

Scrolling back up cleans up the section class in reverse order.

Live demo — same nav, hides on scroll-down

Section titled “Live demo — same nav, hides on scroll-down”

Identical pill styling, only the current indicator (no hover indicator) and an aa-nav="hide change" modifier so it slides up off-screen when you scroll down and returns on scroll-up. Once you’re past 100px the is-scrolled class kicks in too.

aa-nav="hide change" — default aa-distance="1.5" clears any drop-shadow. The current indicator carries aa-duration="0.45" + aa-ease="power3.out".

overview

Scroll down — the nav slides up out of view. Scroll back up and it returns.

approach

Once you’ve scrolled more than 100px, the nav picks up is-scrolled — the demo uses that to add a subtle drop-shadow.

work

The current pill tracks whichever section is in view, morphed via Flip.

contact

The body picks up aa-scroll-direction and aa-scroll-started attributes — the hide rule is just a CSS transform keyed on those.