Now.js Framework Documentation

Now.js Framework Documentation

MenuManager

EN 09 Nov 2026 03:28

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:

  1. The argument of updateActiveMenu(path)
  2. The menuPath of the current route, which comes from the route's config or from a menu_path the API sent (see the next section)
  3. pathname + search of 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-api and by ApiComponent's data-component="api"
  • Put menu_path at the level of the fields the template binds: inside data of successResponse(['data' => ...]), or in the row of successResponse($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 menuPath in its config
  • A menu rendered after the data (such as a sidebar that loads its own data) still selects the menu_path link
  • 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_path from 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

Create menu instance

Parameter Type Description
element HTMLElement Menu container

Returns: string - Menu ID

Open mobile menu

Close mobile menu

Toggle mobile menu

Open submenu

Close submenu

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).

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().

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

Get menu instance

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

<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>
<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);
});