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.
composer require salahhusa9/laravel-menu — the service provider and Menu facade are auto-discovered, so no manual registration is needed on Laravel 9+.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.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.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.
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.
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.Cache::remember(..., 60, ...)) or compute all counts in one query and close over the result.Cache::remember the config array you loop over, not the rendered HTML (rendered HTML will bake in one user's permission-filtered items).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.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.$this->get('/')->assertSee('Billing')) and, more usefully, on absence for a user without the permission — that is the behaviour you actually care about.badge() now accepts a Closure as its name argument; if you already pass a Closure-returning string, you can simplify it.How can I help you explore Laravel packages today?