Skip to content

ES6 Modular Navigation Architecture

The zer0-mistakes navigation JavaScript is organized as ES6 modules under assets/js/modules/navigation/. This modular approach enables tree-shaking, independent unit testing, and easy extension without touching monolithic files.

The navigation bar with a hover dropdown open β€” one of the behaviors (dropdowns, keyboard access, smooth scroll, sidebar state) provided by the navigation modules

Module Overview

assets/js/modules/navigation/
β”œβ”€β”€ index.js         β€” Entry point: imports and initializes all modules
β”œβ”€β”€ config.js        β€” Shared constants (breakpoints, selectors, timing)
β”œβ”€β”€ focus.js         β€” Focus trapping for offcanvas / modals
β”œβ”€β”€ gestures.js      β€” Touch/swipe support for mobile nav
β”œβ”€β”€ keyboard.js      β€” Arrow-key navigation, Escape handling
β”œβ”€β”€ scroll-spy.js    β€” Active section highlighting in sidebars
β”œβ”€β”€ sidebar-state.js β€” Persist collapsed/expanded state (localStorage)
└── smooth-scroll.js β€” Smooth scroll to anchor links

The entry point is imported by assets/js/navigation.js:

// assets/js/navigation.js
import { initNavigation } from './modules/navigation/index.js';

document.addEventListener('DOMContentLoaded', () => {
  initNavigation();
});

Key Features

Hover Dropdowns

Desktop navbar dropdowns open on hover with a short delay to prevent accidental triggers. The delay is a module-local constant in navbar.js; the lg breakpoint that gates hover behaviour comes from config.breakpoints, which syncBreakpointsFromCss() refreshes at runtime from the --zer0-bp-* custom properties so the SCSS tokens stay the single source of truth:

// assets/js/modules/navigation/navbar.js
const TOOLTIP_DELAY = { show: 400, hide: 100 };

// assets/js/modules/navigation/config.js
breakpoints: { sm: 576, md: 768, lg: 992, xl: 1200, xxl: 1400 }

Keyboard Navigation

keyboard.js handles:

Key Action
Tab / Shift+Tab Standard focus movement
Arrow Down / Arrow Up Move between dropdown items
Escape Close dropdown / offcanvas
Enter / Space Activate focused item

sidebar-state.js saves which sidebar sections are expanded to localStorage so the state survives page reloads:

// One key holds every expanded node: `state.storagePrefix` + `state.keys.expandedNodes`
localStorage.setItem('zer0-nav-expanded-nodes', JSON.stringify([...expandedNodeIds]));

Scroll Spy

scroll-spy.js highlights the current section in the table-of-contents as the user scrolls. The active entry is the last heading whose top has crossed the reading line (scroll-padding-top below the viewport top), recomputed on each animation frame from cached heading offsets. See Table of Contents.

Graceful Degradation

Optional browser APIs are feature-detected, so a module degrades instead of throwing. The scroll spy needs nothing beyond scrollY β€” it only reaches for ResizeObserver to re-measure heading offsets when content reflows:

if (typeof ResizeObserver !== 'undefined') {
  this._resizeObserver = new ResizeObserver(this._onReflow);
  this._resizeObserver.observe(content);
}

The navbar renders and works as a standard Bootstrap component even when JavaScript is disabled or fails.

_includes/navigation/navbar.html

The navbar include renders the Bootstrap 5 navbar, populates links from _data/navigation.yml (or the dynamic collection fallback), and outputs the data attributes that the JS modules target.

Adding a Navigation Module

  1. Create assets/js/modules/navigation/my-feature.js:
export function initMyFeature() {
  // implementation
}
  1. Import and call it from index.js:
import { initMyFeature } from './my-feature.js';

export function initNavigation() {
  // … existing init calls …
  initMyFeature();
}

See also

  • [[Navigation]]
  • [[Keyboard Navigation]]
  • [[Features]]