Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Menu Laravel Package

salahhusa9/laravel-menu

Laravel Menu is a lightweight package for defining and rendering navigation menus in Laravel. Build nested items with routes or URLs, highlight active links, and customize Blade output for simpler sidebar and header navigation.

View on GitHub
Deep Wiki
Context7

Getting Started

  1. composer require salahhusa9/laravel-menu — the service provider and Menu facade are auto-discovered, so no manual registration is needed on Laravel 9+.
  2. Publish the config (and views if you want to restyle output): php artisan vendor:publish --tag=laravel-menu-config and --tag=laravel-menu-views. Check the exact tag names in the package's MenuServiceProvider — they are the fastest way to discover what is overridable in your pinned version.
  3. Look here first: config/laravel-menu.php (default render view, active-item class names), then the Menu facade / menu builder class for the available chainable methods (add, route, icon, badge, can, divider, header). The README is accurate for the happy path; the builder class is the source of truth for method signatures.
  4. First use case: register a named menu in a dedicated service provider's boot() and render it in your layout:
// app/Providers/MenuServiceProvider.php
Menu::make('sidebar', function ($menu) {
    $menu->add('Dashboard', route('dashboard'))->icon('fas fa-home');
    $menu->add('Users', route('users.index'))->icon('fas fa-users');
});
{{-- resources/views/layouts/app.blade.php --}}
{!! Menu::render('sidebar') !!}

That is enough to ship: named menus, route-driven URLs, and active-state highlighting with no per-controller wiring.

Implementation Patterns

One provider, many named menus. Keep all definitions in a single MenuServiceProvider (registered in bootstrap/providers.php / config/app.php) rather than scattering Menu::make calls across controllers. Named menus are looked up lazily at render time, which keeps definitions in one diffable place.

Gate items by permission instead of branching in Blade. Build the full menu and let the builder filter it — this is the package's main day-to-day value over hand-rolled <li> soup:

Menu::make('sidebar', function ($menu) {
    $menu->add('Billing', route('billing.index'))->can('view-billing');
    $menu->add('Admin', route('admin.index'))->can('access-admin');
});

The renderer drops items the current user cannot see, so views stay logic-free.

Use the new v1.1.2 Closure badges for live counters. The badge name is now resolved lazily, so you can compute it per render instead of baking a number into a provider that runs once:

$menu->add('Inbox', route('inbox.index'))
    ->badge(fn () => auth()->user()->unreadMessages()->count(), 'danger');

This is the single user-facing change in v1.1.2 (everything else in that release is CI/dependency housekeeping). Strings still work — pass a string when the value is static, a Closure only when it genuinely varies.

Compose repeated structures with a local helper. If you have many sibling items of the same shape (e.g. resource routes), wrap $menu->add(...) in a private method on your provider rather than repeating ->icon()/->can() chains. Menus are ordinary PHP, so plain loops over a config array work well and keep the provider short.

Override the view, not the class. For markup changes (Bootstrap vs Tailwind, adding wire:navigate, wrapping <li> in an Alpine component), publish and edit the package view. Subclassing the builder is rarely worth it; the view is the intended extension seam.

Active-state integration. Point items at route names (->route('users.index')) rather than hardcoded URLs so the package can infer the active item from the current request. For resource controllers, verify whether your version does prefix matching for users.*; if not, set active state explicitly per item.

Gotchas and Tips

  • Providers boot before the authenticated user is reliably available. Anything calling auth()->user() inside Menu::make's closure can see null (or the wrong guard) on some requests. Closure badges (v1.1.2) sidestep this because they resolve at render time — prefer them over eager evaluation for anything user-dependent.
  • N+1 in badges. A Closure badge is evaluated while rendering, once per item. Three unread-count badges means three queries per page. Cache the count (Cache::remember(..., 60, ...)) or compute all counts in one query and close over the result.
  • Menus are rebuilt on every request. There is no built-in caching of the menu tree, and that is usually fine — but if a menu is large and static, Cache::remember the config array you loop over, not the rendered HTML (rendered HTML will bake in one user's permission-filtered items).
  • Escape custom labels yourself. Menu output is rendered unescaped to allow icons/markup; if you interpolate user-supplied text into a label or badge, sanitize it before it reaches the builder.
  • route() at provider boot vs. route caching. Calling route() inside boot() is safe because routes are registered by then, but never call route() from a config file — php artisan config:cache will break it. Keep menu definitions in a provider, not in config/laravel-menu.php.
  • Active-state false positives on prefix-overlapping routes (admin.index lighting up while on admin.users.index). Check the generated class in the DOM; if it is wrong, the reliable fix is explicit per-item active handling rather than fighting the default matcher.
  • Publishing views is a one-way door. Once published, upstream template fixes no longer reach you. Publish only when restyling, and note the version you forked from.
  • Testing: assert on the rendered output ($this->get('/')->assertSee('Billing')) and, more usefully, on absence for a user without the permission — that is the behaviour you actually care about.
  • Upgrading to v1.1.2 is safe from v1.1.1: no breaking changes, no deprecations, no config or view signature changes. The only behavioural delta is that badge() now accepts a Closure as its name argument; if you already pass a Closure-returning string, you can simplify it.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode
jjsquady/round-robin
skyraptor/chatter
edlugz/sasapay
abbasudo/artisan-route