ngx-multi-level-push-menu

July 12, 2026 · View on GitHub

npm version CI Live demo License: MIT

Accessible, responsive, SSR-safe multi-level push navigation for Angular. The library is standalone-first, has no icon or animation dependency, and still supports existing NgModule applications.

Open the interactive demo to try every public input, targeted service commands, typed output events, RTL, theming, and copy-ready setup examples.

Why use it?

  • One declarative menu tree; no imperative DOM construction
  • Standalone component with zero required providers
  • Typed item, group, level, and collapsed-state events
  • Optional targeted service commands for applications with one or many menus
  • Native buttons and links, keyboard navigation, focus management, live announcements, RTL, reduced motion, and forced-colors support
  • Pointer-based swipe gestures that preserve vertical scrolling
  • Safe inline path icons or CSS classes from any icon library
  • Signals and OnPush change detection; compatible with zoneless applications
  • Guarded browser APIs for server-side rendering and hydration
  • Cover and overlap layouts with responsive behavior

Compatibility

Library lineAngular peer rangeRxJS peer rangeNode.js
20.x (current declarative line)>=20 <23 (Angular 20, 21, and 22)>=7.8 <8Follow your Angular major's support
19.x (legacy)Angular 19See the release metadataSee the release metadata
<=18.x (archived)See the exact npm release metadataSee the release metadataSee the release metadata

Older library major numbers did not consistently match Angular major numbers. Do not infer compatibility from the package version; inspect the chosen release's peerDependencies. For the current line, use the Node.js version supported by your Angular major. The repository development toolchain uses Node >=20.19 <25 and npm 11.

Angular 19 applications must remain on @ramiz4/ngx-multi-level-push-menu@^19 until the application is upgraded to Angular 20 or newer. That legacy line no longer receives supported security fixes after 20.x publishes, so plan the Angular upgrade; do not force-install the 20.x library over an incompatible peer range.

Installation

npm install @ramiz4/ngx-multi-level-push-menu

There are no required Font Awesome, Angular Animations, or application-level stylesheet imports.

Quick start: standalone

Import the standalone component. No provider function is required.

import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import { MenuActivationEvent, MultiLevelPushMenuComponent, MultiLevelPushMenuItem, MultiLevelPushMenuOptions } from '@ramiz4/ngx-multi-level-push-menu';

@Component({
  selector: 'app-shell',
  standalone: true,
  imports: [MultiLevelPushMenuComponent, RouterOutlet],
  templateUrl: './app-shell.component.html',
  styleUrl: './app-shell.component.scss',
})
export class AppShellComponent {
  readonly menu: readonly MultiLevelPushMenuItem[] = [
    { id: 'home', name: 'Home', link: '/' },
    {
      id: 'products',
      name: 'Products',
      items: [
        { id: 'new', name: 'New products', link: '/products/new' },
        { id: 'all', name: 'All products', link: '/products' },
      ],
    },
    { id: 'help', name: 'Help', link: 'https://example.com/help', target: '_blank' },
  ];

  readonly options = new MultiLevelPushMenuOptions({
    title: 'Acme',
    ariaLabel: 'Primary navigation',
    menuWidth: '19rem',
    mode: 'cover',
    closeOnNavigation: true,
  });

  collapsed = false;

  onItemActivate(event: MenuActivationEvent): void {
    console.log(event.item, event.level, event.path, event.originalEvent);
  }
}
<ngx-multi-level-push-menu [menu]="menu" [options]="options" [(collapsed)]="collapsed" (itemActivate)="onItemActivate($event)">
  <router-outlet />
</ngx-multi-level-push-menu>

Give the component (or its containing layout) a height. menuHeight defaults to 100%.

:host,
ngx-multi-level-push-menu {
  display: block;
  height: 100dvh;
}

ngx-multi-level-push-menu is the canonical selector. The historical ramiz4-multi-level-push-menu selector remains available as a compatibility alias for existing templates.

The deprecated provideMultiLevelPushMenu() helper remains exported only for source compatibility. Remove it when convenient: MultiLevelPushMenuService is provided in root.

Existing NgModule applications

The NgModule API remains available for compatibility:

import { NgModule } from '@angular/core';
import { NgxMultiLevelPushMenuModule } from '@ramiz4/ngx-multi-level-push-menu';

@NgModule({
  imports: [NgxMultiLevelPushMenuModule],
})
export class AppModule {}

NgxMultiLevelPushMenuModule.forRoot() also continues to work. Both the module and forRoot() are compatibility APIs; new code should import MultiLevelPushMenuComponent directly, even inside an NgModule.

MultiLevelPushMenuItem<TData> accepts application-specific data while keeping the navigation fields typed.

interface NavMetadata {
  permission?: string;
  analyticsId: string;
}

const menu: MultiLevelPushMenuItem<NavMetadata>[] = [
  {
    id: 'reports',
    name: 'Reports',
    ariaLabel: 'Open reports',
    icon: '<svg viewBox="0 0 24 24"><path d="M4 20V10h4v10M10 20V4h4v16M16 20v-7h4v7" fill="none" stroke="currentColor" stroke-width="2"/></svg>',
    link: '/reports',
    data: { permission: 'reports.read', analyticsId: 'nav-reports' },
  },
];
FieldTypeMeaning
namestringPreferred visible label
titlestringFallback label; retained for older menu models
idstringStable identity and service navigation target
iconstringSafe SVG path document or CSS class list
linkstringInternal Angular URL, external URL, fragment, or other anchor URL
itemsMultiLevelPushMenuItem<TData>[]Child items; a non-empty array makes the item a group
disabledbooleanRenders a non-navigating disabled control and prevents activation
ariaLabelstringAccessible name override
targetstringNative anchor target, such as _blank
relstringNative anchor relationship; _blank gets noopener noreferrer by default
dataTDataConsumer-owned metadata

The visible-label fallback order is name, title, id, then Untitled item. Use immutable updates (a new menu array or options object) when changing inputs in an OnPush application. If both [menu] and options.menu are supplied, the explicit [menu] input wins.

Routing behavior

  • Same-context internal links use the optional Angular Router when one is available.
  • Rendered internal anchor URLs include the application's base href, so open-in-new-tab and modified clicks also work from subpath deployments.
  • External, protocol-relative, fragment, modified-click, and non-_self links keep native anchor behavior.
  • target="_blank" is protected with rel="noopener noreferrer" unless rel is supplied.
  • Set closeOnNavigation to collapse after native navigation starts or Angular Router navigation succeeds.
  • The legacy-named preventItemClick: false option bypasses Angular Router interception. Component and service click events still emit.

Component API

Inputs

InputTypeDefaultNotes
menureadonly MultiLevelPushMenuItem[] | null | undefinedUses options.menuExplicit menu tree; once bound, it takes precedence over options.menu
optionsMultiLevelPushMenuOptions | Partial<MultiLevelPushMenuOptions> | null | undefinedClass defaultsEach assignment is merged over fresh defaults
collapsedboolean | null | undefinedoptions.collapsedSupports [(collapsed)]; null and undefined do not change state

Outputs

OutputPayloadWhen it emits
collapsedChangebooleanThe collapsed state changes through user or public API interaction
menuOpenbooleanThe menu expands (true)
menuClosebooleanThe menu collapses (true)
itemClickMultiLevelPushMenuItemA leaf item activates; compatibility event
groupItemClickMultiLevelPushMenuItemA group opens; compatibility event
itemActivateMenuActivationEventA leaf activates, with item, zero-based level, full path, and original event
groupActivateMenuActivationEventA group activates, with item, level, path, and original event
levelChangenumberActive zero-based depth changes; root is 0

Prefer itemActivate and groupActivate when you need context. The shorter click outputs remain useful for existing consumers.

Public component methods

Obtain a component instance with @ViewChild(MultiLevelPushMenuComponent) when controls belong to the same view.

MethodEffect
collapseMenu(level?)Collapse the menu, or navigate back to an already-open numeric depth
expandMenu()Expand and move focus into the active level
toggleMenu()Toggle expanded/collapsed state
openMenu()Alias for expandMenu()
closeMenu()Alias for collapseMenu()
navigateToLevel(levelOrId)Navigate to an open depth, or find a group by id, then name/title
goBack(focusParent = true)Return one level and optionally restore focus to the parent group

collapseMenu and expandMenu also accept a legacy animation-speed argument. Animation timing is configured by animationDuration; the speed argument is retained only for source compatibility.

Service API and menu targeting

MultiLevelPushMenuService is useful when the controller is outside the menu's view. Inject it normally; no provider setup is required.

import { inject } from '@angular/core';
import { MultiLevelPushMenuService } from '@ramiz4/ngx-multi-level-push-menu';

const menus = inject(MultiLevelPushMenuService);

menus.openMenu('primary');
menus.navigateToLevel('products', 'primary');
menus.goBack('primary');
menus.closeMenu('primary');

Set options.menuID on each menu to target it. A command without targetId is broadcast to every mounted menu using that service instance.

MethodSignatureNotes
collapse(level?: number, targetId?: string) => voidCollapse, or move to an already-open depth; use collapse(undefined, 'primary') to target a normal collapse
expand(targetId?: string) => voidExpand
toggleMenu(targetId?: string) => voidToggle
openMenu(targetId?: string) => voidAlias for expand
closeMenu(targetId?: string) => voidAlias for targeted collapse
navigateToLevel(levelOrId: number | string, targetId?: string) => voidNavigate to an open depth or group identity
goBack(targetId?: string) => voidReturn one level

Compatibility observables are also available: collapsed(), expanded(), onMenuItemClick(), and onGroupItemClick(). The item streams emit typed MultiLevelPushMenuItem values.

Options and defaults

Create options with new MultiLevelPushMenuOptions({...}) or pass a Partial<MultiLevelPushMenuOptions> directly.

While expanded, the area outside the visible drawer acts as a close target and does not activate the covered page underneath. cover translates the full-width page away from the drawer; overlap leaves it stationary below the drawer.

OptionTypeDefaultBehavior
menuMultiLevelPushMenuItem[][]Menu fallback when [menu] is not bound
mode'cover' | 'overlap''cover'Cover translates full-width content; overlap overlays unchanged content and adds clickable ancestor rails
collapsedbooleanfalseApplied when this configured value changes; a separate [collapsed] input takes precedence
menuIDstring | undefinedundefined<nav> ID and service-command target
wrapperClassstring | undefinedundefinedExtra class on the rendered wrapper
menuInactiveClassstring | undefinedundefinedExtra wrapper class while collapsed
menuWidthstring | number'300px'Any CSS length; a number is pixels
menuHeightstring | number'100%'Any CSS length; a number is pixels
titlestring | undefinedundefinedRoot heading; visually falls back to Menu
titleIconstringBuilt-in bars SVGHeader and toggle icon
backTextstring'Back'Back-button text
backItemClassstring'back-item'Extra class on the Back button
backItemIconstringBuilt-in chevron SVGBack-button icon
groupIconstringBuilt-in chevron SVGGroup indicator
defaultItemIconstringBuilt-in dot SVGFallback icon for items without a custom icon
overlapWidthstring | number55Width of each ancestor rail; a number is pixels and also influences swipe threshold
preventItemClickbooleantrueLegacy routing flag; false uses native anchor navigation instead of Angular Router interception
preventGroupItemClickbooleantrueLegacy propagation flag; true stops the group activation DOM event from bubbling. Outputs still emit
direction'ltr' | 'rtl''ltr'Layout, swipe, and forward/back keyboard direction
fullCollapsebooleanfalseHide navigation completely instead of leaving the full-height item-icon rail visible
swipe'both' | 'left' | 'right' | 'touchscreen' | 'desktop' | 'none''both'Enabled pointer kinds/directions; touchscreen and desktop are compatibility aliases
ariaLabelstring'Main navigation'Accessible label for the <nav> landmark
closeOnNavigationbooleantrueCollapse after leaf navigation
preserveActiveLevelOnCollapsebooleantrueKeep the submenu stack across collapse/expand
maxDepthnumber50Maximum submenu depth; also limits malformed/cyclic data
animationDurationstring | number500CSS time or milliseconds when numeric

Icons

Icons are optional and dependency-free.

Safe inline SVG paths

An icon string beginning with < must be a complete <svg> containing at least one safe <path>. The renderer does not use innerHTML: it parses a numeric four-value viewBox plus safe path and paint attributes, then creates Angular-owned SVG elements. Scripts, event handlers, external references, unsupported elements, invalid path data, and overlong paths are discarded. An invalid SVG renders no icon.

const icon = '<svg viewBox="0 0 24 24"><path d="M4 12h16M12 4v16" fill="none" stroke="currentColor" stroke-width="2"/></svg>';

The supported path attributes are d, fill, stroke, stroke-width, fill-rule, and clip-rule. Use simple path-only SVGs; convert shapes such as circles or polygons to paths first.

CSS icon classes

Any non-markup string is applied as a CSS class list:

{ name: 'Settings', icon: 'fa-solid fa-gear', link: '/settings' }

Install and load the chosen icon library in your application. This package does not bundle one.

Styling

Set custom properties directly on the component host. This works in an application component stylesheet and can be scoped per instance with a class.

<ngx-multi-level-push-menu class="app-menu" />
ngx-multi-level-push-menu.app-menu {
  --ngx-push-menu-background: #0b3d2e;
  --ngx-push-menu-surface: #115740;
  --ngx-push-menu-hover: #176b4e;
  --ngx-push-menu-active: #082d22;
  --ngx-push-menu-color: #fff;
  --ngx-push-menu-border: rgb(255 255 255 / 24%);
  --ngx-push-menu-focus: #a7f3d0;
  --ngx-push-menu-shadow: 0 0.75rem 2rem rgb(0 0 0 / 30%);
}
CSS custom propertyDefault
--ngx-push-menu-background#336ca6
--ngx-push-menu-surface#2e6196
--ngx-push-menu-hover#295685
--ngx-push-menu-active#1f4164
--ngx-push-menu-color#fff
--ngx-push-menu-borderrgb(255 255 255 / 18%)
--ngx-push-menu-focus#fff
--ngx-push-menu-shadow0 0.5rem 1.5rem rgb(0 0 0 / 24%)

Sizing and motion are configured through menuWidth, menuHeight, overlapWidth, and animationDuration; the component exposes them internally as --ngx-push-menu-width, --ngx-push-menu-height, --ngx-push-menu-overlap, and --ngx-push-menu-duration.

Accessibility and interaction

The component provides:

  • A labeled navigation landmark and native interactive elements
  • Disabled semantics and safe external-link defaults
  • Up/Down looping within a level; Home/End to jump; direction-aware Left/Right for submenus; Escape to go back or collapse
  • Focus movement into opened levels and restoration to the parent group
  • Polite live announcements for level and collapsed-state changes
  • Inert, non-focusable inactive panels
  • Logical CSS properties for LTR/RTL layouts
  • prefers-reduced-motion and Windows forced-colors adaptations
  • Vertical-scroll-friendly pointer gestures using touch-action: pan-y

Accessibility still depends on consumer data and theming. Give every menu a useful ariaLabel, use meaningful item labels, keep IDs unique, and verify color contrast and keyboard flows in the consuming application.

SSR, hydration, and zoneless Angular

  • The initial menu tree renders on the server.
  • Browser-only focus scheduling is guarded with isPlatformBrowser and the injected DOCUMENT.
  • The Router is optional, so rendering does not require router providers.
  • Pointer handling runs only in response to browser events.
  • The implementation uses signals, Angular events, and OnPush; it does not require Zone.js from the consuming application.

No special provider is needed for SSR, hydration, or zoneless change detection. Configure those features as usual in the Angular application.

Advanced public directives

The main component already handles items and swipes. MenuItemDirective ([ramiz4MenuItem]) and SwipeDirective ([ramiz4Swipe]) remain exported for custom templates. They emit typed MenuItemClickEvent, KeyNavigationEvent, and SwipeEvent values. Treat them as advanced building blocks; most applications only import MultiLevelPushMenuComponent.

Troubleshooting

The menu has no height

The default menuHeight is 100%, so an ancestor must establish a height. Set a host height or pass an explicit value such as 100dvh.

The service controls every menu

Commands without a target are broadcasts. Give each menu a unique menuID and pass that ID to service methods.

A submenu does not open

Use an items array, make sure the item is not disabled, and avoid cyclic object/array references. Navigation is capped by maxDepth.

An icon is blank

For inline icons, use one complete path-only SVG that meets the safe parser rules. Otherwise pass CSS classes and ensure that icon library is loaded globally.

Router navigation is not intercepted

Confirm that the application provides Angular Router, the link is an internal same-context URL, target is absent or _self, and preventItemClick is not false.

Development

npm ci
npm start                 # example app on http://localhost:4200
npm run test:lib
npm run lint
npm run build:lib
npm run build:app
npm run validate          # format, lint, CI tests, library and example builds

See CONTRIBUTING.md for the contributor workflow, MAINTENANCE.md for releases, MIGRATION.md for upgrading, and CHANGELOG.md for notable changes.

Support and security

License

MIT © Ramiz Loki