Headless Core — Navbar
Powers Navbar internally. The most feature-rich navigation factory — beyond simple active-item tracking, it can derive the active item from scroll position, a URL hash, or a route pathname, so a single config drives a marketing-site nav, a docs sidebar-style top nav, or an SPA router nav.
import { createNavbar } from '@andersseen/headless-components/navbar';
const navbar = createNavbar({ items: [ { id: 'home', label: 'Home', href: '#hero' }, { id: 'features', label: 'Features', href: '#features' }, ], defaultActiveItem: 'home', onActiveItemChange: id => console.log('Active:', id),});
element.addEventListener('keydown', e => navbar.handleItemKeyDown(e, 'home'));window.addEventListener('scroll', () => navbar.actions.updateActiveFromScroll(),);Config
Section titled “Config”| Option | Type | Default | Notes |
|---|---|---|---|
items | NavbarItem[] | [] | { id, label, href?, target?, icon?, disabled? }. |
defaultActiveItem | string | first item id, or 'home' | |
onActiveItemChange | (id: string) => void | — | |
mobileMenuOpen | boolean | false | |
onMobileMenuChange | (open: boolean) => void | — | |
scrollSpy | boolean | false | Items must have href starting with # to participate. |
scrollSpyOffset | number (px) | 100 | Section counts as “in view” once scrolled past this line from the top. |
ariaLabel | string | 'Main navigation' | |
routeMatchMode | 'exact' | 'prefix' | 'prefix' | prefix matches nested routes (/docs/intro → /docs), preferring the longest match. |
getElementByHash | (hash: string) => Element | null | document.querySelector | Adapter so the headless core never touches the DOM directly. |
getLocation | () => { hash: string; pathname: string } | window.location | Same reasoning — swap in a router’s location for SPA frameworks. |
activeItem, mobileMenuOpen, itemIds: string[].
Actions
Section titled “Actions”| Member | Signature | Notes |
|---|---|---|
setActiveItem(id) | (itemId: string) => void | No-ops for a disabled item. |
toggleMobileMenu() / setMobileMenuOpen(v) / closeMobileMenu() | () => void / (boolean) => void / () => void | |
setItems(items) | (items: NavbarItem[]) => void | Re-registers the item list; resets activeItem if it’s no longer present. |
updateActiveFromScroll(offset?) | (offset?: number) => void | Call from a scroll listener. |
updateActiveFromHash() | () => void | Call on load / hashchange. |
updateActiveFromRoute(pathname?, mode?) | (pathname?: string, routeMatchMode?: 'exact' | 'prefix') => void | Call on route change; pass pathname explicitly in SPA frameworks. |
Queries
Section titled “Queries”isActive(id), getActiveItem(), isDisabled(id), getItemIds().
Prop-getters & handler
Section titled “Prop-getters & handler”| Getter | Returns |
|---|---|
getContainerProps() | role: 'navigation', aria-label |
getNavListProps() | role: 'menubar', aria-label |
getItemProps(id, { href?, target? }) | role: 'menuitem', aria-current ('page' when active), aria-disabled, data-active, data-state, tabindex (roving), id, plus href/target if passed |
getToggleProps() | aria-expanded, aria-label (swaps text open/closed), aria-controls, role: 'button', tabindex: 0, data-state |
getMobileMenuProps() | id, role: 'menu', aria-label, data-state, hidden |
handleItemKeyDown(event, currentItemId) — ArrowRight/ArrowLeft move with
wraparound (skipping disabled items), Home/End jump, Enter/Space
activate the current item and close the mobile menu, Escape closes the mobile
menu.
Live example
Section titled “Live example”Next steps
Section titled “Next steps”Sidebar is the same active-item-tracking idea for a vertical side panel, with a collapse toggle instead of scroll-spy/route detection.