Skip to content

Nav — JavaScript

ts
import { Nav } from "@popovandrii/ui-elements";
import "@popovandrii/ui-elements/style.css";

const nav = new Nav();

One call binds every .UInav on the page. The markup is on the HTML & CSS page.

Constructor

ts
new Nav(selectors?, debug?, options?);
OptionDefault
rootdocumentlimit scanning to a subtree
observefalsedebounced MutationObserver re-scan
closeOnNavigatetrueclose the drawer when a link inside the panel is clicked

Leave closeOnNavigate on unless you have a reason: without it a hash or SPA navigation leaves the drawer sitting on top of the view it just navigated to.

ts
new Nav({}, false, { root: document.querySelector("#app")!, observe: true });

Methods

Method
open(el) / close(el) / toggle(el)drive the drawer
isOpen(el)boolean
scan()bind newly added .UInavs — safe to call repeatedly
destroy()release scan-scoped listeners; the manager stays reusable
dispose()destroy() plus the lifetime-scoped Escape / Tab / resize listeners

Only one drawer is open at a time — opening a second closes the first.

ts
const bar = document.querySelector<HTMLElement>("#site-nav")!;

nav.open(bar);
nav.isOpen(bar); // true
nav.close(bar);

Events

Both bubble and carry the nav's id:

ts
document.addEventListener("ui-nav-open", (e) => {
  console.log((e as CustomEvent).detail.id);
});
document.addEventListener("ui-nav-close", () => {
  /* … */
});

Auto-hide

Add .UInav--auto-hide to a sticky bar and the manager takes it from there — it watches the page scroll and toggles .UInav--hidden. The markup side is on the HTML & CSS page; what the manager guarantees:

  • the scroll listener is passive and does nothing but queue a frame — scrollY is read inside requestAnimationFrame, off the scroll thread's critical path;
  • a 6px threshold absorbs momentum and rubber-band, so the bar doesn't twitch;
  • an open drawer pins the bar in place, and open() brings a hidden bar back before it measures it — the drawer hangs off the bar's real bottom edge.

Both class names are in the selectors map, so you can rename them like any other:

ts
new Nav({ autoHide: "site-header--auto-hide", hidden: "site-header--hidden" });

Singleton factory

ts
import { getNavManager, resetManagers } from "@popovandrii/ui-elements";

getNavManager(); // one shared manager for the page
resetManagers(); // tear every manager down (Nav gets the full dispose())

What the manager does to the page while the drawer is open

The drawer is modal, so opening it isn't a local change:

  • everything outside the nav gets inert — pointer, Tab and screen reader alike;
  • document.body stops scrolling, and the scrollbar's gutter is paid back as padding so the layout doesn't jump sideways;
  • Escape closes from anywhere; focus lands in the drawer on open and returns to the toggle on close;
  • Tab is trapped across the whole bar — the close button and the pinned controls sit outside the panel, and trapping them out of reach would strand the user in the drawer.

All of it is undone on close, and on destroy() even if the drawer was open.

Two things are deliberately left alone:

  • Toasts are never inerted — they outrank the drawer (z-index: 9999) and can arrive while it's open, so inerting them would render a notification nobody can dismiss.
  • An inert you set yourself stays set; the manager only removes its own.

SPA lifecycle

ts
const nav = new Nav({}, false, { root: view, observe: true });

// on unmount
nav.dispose();

destroy() un-freezes the page and drops the backdrop it created, then leaves the manager ready for another scan(). Use dispose() when the manager itself is going away.