Now.js Framework Documentation
MenuManager
MenuManager
Overview
MenuManager is the navigation menu management system in Now.js Framework. It supports responsive menus, submenus, and touch devices.
When to use:
- Need navigation menu
- Need responsive hamburger menu
- Need dropdown submenus
- Need active state tracking
Why use it:
- ✅ Responsive (mobile/desktop)
- ✅ Dropdown submenus
- ✅ Touch gesture support
- ✅ Keyboard navigation
- ✅ ARIA accessibility
- ✅ Auto-active state
Basic Usage
HTML Structure
<nav data-component="menu">
<button class="menu-toggle">☰</button>
<ul class="menu">
<li><a href="/">Home</a></li>
<li>
<a href="/products">Products</a>
<ul>
<li><a href="/products/new">New</a></li>
<li><a href="/products/sale">Sale</a></li>
</ul>
</li>
<li><a href="/about">About</a></li>
<li><a href="/contact">Contact</a></li>
</ul>
</nav>Initialization
await MenuManager.init({
breakpoint: 768,
animationDuration: 200
});Configuration
MenuManager.init({
// Mobile breakpoint (px)
breakpoint: 768,
// Animation duration (ms)
animationDuration: 100,
// Touch swipe threshold (px)
touchThreshold: 100,
// Accessibility
accessibility: {
enabled: true,
announceChanges: true,
smoothFocus: true
},
// Hover delays
hover: {
openDelay: 100,
closeDelay: 300
}
});Data Attributes
| Attribute | Description |
|---|---|
data-component="menu" |
Initialize menu |
data-source |
API source for menu items |
Active menu item
On every route change and every time a menu is rendered, MenuManager finds the links matching the current page and gives them the active class and aria-current="page". The path it compares is the first one available, in this order:
- The argument of
updateActiveMenu(path) - The
menuPathof the current route, which comes from the route's config or from amenu_paththe API sent (see the next section) pathname + searchof the address bar
A link with a query string matches only when every query parameter matches (in any order); a link without one compares the path alone. When no link matches, the link with the longest path that is a prefix of the current path wins.
Letting the API choose the menu item (menu_path)
A detail page usually belongs to one of several filtered menu links — the menu has /orders?status=QUO and /orders?status=INV, while the detail page's address is just /order?id=248 and says nothing about the document's type.
The route can only say menuPath: '/orders', which matches no link, so the first link always wins. Only the server knows which menu item the record belongs to.
When the page's data carries menu_path, that value replaces the menuPath of the current route:
// api/inventory/order/get
$order['menu_path'] = '/inventory-orders?status='.$order['document_type'];
return $this->successResponse([
'data' => $order,
'options' => $options
], 'OK');- Works with data loaded by FormManager's
data-load-apiand by ApiComponent'sdata-component="api" - Put
menu_pathat the level of the fields the template binds: insidedataofsuccessResponse(['data' => ...]), or in the row ofsuccessResponse($row)— both are read - Absent or empty = the menu item is chosen as before, by the order above
- It lasts until the page is left: the router builds a new current route on every navigation, so the next page goes back to the
menuPathin its config - A menu rendered after the data (such as a sidebar that loads its own data) still selects the
menu_pathlink - A response that arrives after the user has left that page does not touch the new page's menu
- The value must be a link that exists in the menu; when none matches, the longest-prefix rule applies as before
- A
menu_pathfrom a modal's API moves the page's menu too, so send it only from the API that holds the page's own data
API Reference
MenuManager.createMenu(element)
Create menu instance
| Parameter | Type | Description |
|---|---|---|
element |
HTMLElement | Menu container |
Returns: string - Menu ID
MenuManager.openMenu(menu)
Open mobile menu
MenuManager.closeMenu(menu)
Close mobile menu
MenuManager.toggleMenu(menu)
Toggle mobile menu
MenuManager.openSubmenu(menu, button, options?)
Open submenu
MenuManager.closeSubmenu(menu, button)
Close submenu
MenuManager.closeAllSubmenus(menu, options?)
Close all submenus · {keepActive: true} leaves open the submenus that hold the active link
On a page change (route:changed) MenuManager selects the active link again, then closes the other submenus with keepActive — the branch holding the current page stays expanded even when the menu is not rebuilt (a sidebar in a RouterManager layout).
MenuManager.updateActiveMenu(path)
Selects the active links again by the rules in Active menu item, emitting menu:activeChanged when the selection changed
| Parameter | Type | Description |
|---|---|---|
path |
string | (optional) Path to compare; when omitted, the current route's menuPath or the address bar is used |
Returns: Element[] - The active links
MenuManager.updateActiveMenu('/products/new');When the selected link changes, the submenus around it expand (their items become reachable with Tab), and if the link sits in a scrollable .sidemenu (an ancestor box with overflow-y: auto|scroll whose content overflows) that box is scrolled to show it in the next frame — the page itself never scrolls.
A path passed as the argument applies to this call only; a menu rendered later goes back to the route. To keep a value for the whole page, use applyMenuPath().
MenuManager.applyMenuPath(payload)
When the data carries menu_path (at its top level or inside data), sets it as the current route's menuPath and selects the menu item again; otherwise does nothing.
FormManager and ApiComponent call it after loading data, so it rarely needs calling by hand — see Letting the API choose the menu item.
| Parameter | Type | Description |
|---|---|---|
payload |
Object | Data the page loaded |
Returns: boolean - true when the data named a menu_path
MenuManager.getMenu(menuId)
Get menu instance
MenuManager.hasMenu(menuId)
Check if menu exists
Keyboard Navigation
| Key | Action |
|---|---|
↓ / → |
Next item |
↑ / ← |
Previous item |
Enter / Space |
Activate/toggle |
Escape |
Close submenu |
Tab |
Navigate focus |
CSS Styling
/* Menu container */
[data-component="menu"] {
position: relative;
}
/* Menu list */
.menu {
display: flex;
list-style: none;
margin: 0;
padding: 0;
}
.menu > li {
position: relative;
}
.menu a {
display: block;
padding: 12px 16px;
color: inherit;
text-decoration: none;
}
.menu a:hover {
background: rgba(0,0,0,0.05);
}
/* Active state */
.menu a.active {
color: #3b82f6;
font-weight: 500;
}
/* Submenu */
.menu ul {
position: absolute;
top: 100%;
left: 0;
min-width: 200px;
background: white;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
opacity: 0;
visibility: hidden;
transition: all 0.2s;
}
.menu li:hover > ul {
opacity: 1;
visibility: visible;
}
/* Mobile toggle */
.menu-toggle {
display: none;
}
/* Mobile responsive */
@media (max-width: 768px) {
.menu-toggle {
display: block;
}
.menu {
position: fixed;
top: 0;
left: -280px;
width: 280px;
height: 100vh;
flex-direction: column;
background: white;
transition: left 0.3s;
}
.menu.open {
left: 0;
}
.menu ul {
position: static;
box-shadow: none;
}
}Real-World Examples
Main Navigation
<header>
<nav data-component="menu">
<a href="/" class="logo">Logo</a>
<button class="menu-toggle" aria-label="Menu">
<span></span>
<span></span>
<span></span>
</button>
<ul class="menu">
<li><a href="/">Home</a></li>
<li>
<button aria-expanded="false">Products ▼</button>
<ul>
<li><a href="/products/software">Software</a></li>
<li><a href="/products/hardware">Hardware</a></li>
<li><a href="/products/services">Services</a></li>
</ul>
</li>
<li><a href="/pricing">Pricing</a></li>
<li><a href="/contact">Contact</a></li>
</ul>
</nav>
</header>Data-Driven Menu
<nav data-component="menu" data-source="api.menus">
<!-- Menu will be rendered from API data -->
</nav>
<script>
// API response structure
const menus = [
{ label: 'Home', href: '/' },
{
label: 'Products',
children: [
{ label: 'New', href: '/products/new' },
{ label: 'Sale', href: '/products/sale' }
]
}
];
</script>Sidebar Menu
<aside data-component="menu" class="sidebar-menu">
<ul class="menu vertical">
<li><a href="/dashboard">Dashboard</a></li>
<li>
<button>Settings</button>
<ul>
<li><a href="/settings/profile">Profile</a></li>
<li><a href="/settings/security">Security</a></li>
</ul>
</li>
<li><a href="/logout">Logout</a></li>
</ul>
</aside>Common Pitfalls
⚠️ 1. Submenu Must Be Inside li
<!-- ❌ Submenu in wrong place -->
<ul class="menu">
<a href="#">Products</a>
<ul>...</ul>
</ul>
<!-- ✅ Submenu inside li -->
<ul class="menu">
<li>
<a href="#">Products</a>
<ul>...</ul>
</li>
</ul>⚠️ 2. ARIA for Accessibility
<!-- ❌ No ARIA -->
<button>Menu</button>
<!-- ✅ With ARIA -->
<button aria-expanded="false" aria-controls="main-menu">
Menu
</button>⚠️ 3. MenuManager only supports 2 levels of nesting
The buildMenu() loop uses if (level < 2) — it renders children only for level 0 (top) and level 1 (first submenu).
A third nesting level is silently ignored.
// ❌ 3-level nesting — third level never renders
$menus['settings'] = [
'title' => 'Settings',
'children' => [
[
'title' => 'Widgets', // level 1
'children' => [ // level 2 — IGNORED
['title' => 'Facebook', 'url' => '/widgets/facebook'],
]
]
]
];
// ✅ Keep Widgets as a separate top-level group
$menus['settings'] = [
'title' => 'Settings',
'children' => [
['title' => 'General', 'url' => '/general-settings'],
...
]
];
$menus['widgets'] = [ // top-level group
'title' => 'Widgets',
'icon' => 'icon-widgets',
'children' => [
['title' => 'Facebook', 'url' => '/widgets/facebook', 'icon' => 'icon-facebook'],
['title' => 'Text links', 'url' => '/widgets/textlinks', 'icon' => 'icon-ads'],
]
];Events
| Event | When Triggered | Detail |
|---|---|---|
menu:opened |
A menu opened | {menu} |
menu:closed |
A menu closed | {menu} |
submenu:opened |
A submenu opened | {menu, button, submenu, previousFocus} |
submenu:closed |
A submenu closed | {menu, button, submenu, currentFocus} |
menu:activeChanged |
The active item changed with the route | {path, hash, activeLinks, previousActive} |
menu:rendered |
A menu was rendered from data | {container, itemCount} |
menumanager:destroyed |
destroy() completed | none |
EventManager.on('menu:activeChanged', ({path, activeLinks}) => {
console.log('Active menu changed:', path, activeLinks.length);
});Related Documentation
- RouterManager - SPA routing
- AnimationManager - Animations