kalnoy/illuminate-menu
A fluent menu builder for Laravel: define nested menus in PHP and render them as HTML. Items support attributes, active-state detection, ordering, and custom views, giving you a clean, reusable API for building navigation in views or service providers.
Install
composer require kalnoy/illuminate-menu
On Laravel 5.5+ the Kalnoy\Menu\MenuServiceProvider is auto-discovered. On older versions add it to config/app.php and register the facade alias:
'providers' => [Kalnoy\Menu\MenuServiceProvider::class],
'aliases' => ['Menu' => Kalnoy\Menu\Facade::class],
Publish the config if the package ships one (php artisan vendor:publish --provider="Kalnoy\Menu\MenuServiceProvider") — this is where the active-item / URL-comparison settings live.
Where to look first (in this order)
README.md — short, but it is the only real documentation for this package.config/menu.php — the active-detection settings that decide which item gets the active class.src/Builder.php — the fluent API surface (add, dropdown, raw, render).src/Item.php and the traits it composes (HasUrl, HasTitle, HasIcon, HasAttributes, Activatable) — this is where every ->icon() / ->attributes() / ->active() call actually resolves.First use case: a navbar in a service provider
// app/Providers/AppServiceProvider.php (boot)
Menu::make('navbar', function ($menu) {
$menu->add('Home', '/');
$menu->add('About', route('about'));
$menu->dropdown('Products', function ($sub) {
$sub->add('Plans', route('plans'));
$sub->add('Pricing', route('pricing'));
});
});
{!! Menu::get('navbar') !!}
That is the whole loop: define once, render anywhere. The package emits Bootstrap 3 nav markup (nav navbar-nav, dropdown-menu, active) so the first thing to verify is that it looks right against your actual Bootstrap version.
Define menus in one place, not in views. Keep a dedicated MenuServiceProvider (or a Menu bootstrapper class) rather than scattering Menu::make() calls across controllers. Views then contain a single {!! Menu::get('navbar') !!} line and are trivially reusable in layouts and mail/partials.
Prefer route() names over hardcoded URLs. Active-state detection works by comparing the current request against each item's URL. Using named routes keeps the comparison stable when you change paths, and it makes the menu testable ($this->get(route('pricing'))).
Nest with dropdown() and keep the closure flat. Nested dropdowns render, but Bootstrap 3's markup doesn't support submenus well, so treat it as one level. If you need deeper trees, build the structure elsewhere and only use this package for the top-level chrome.
Authorization-aware menus. Filter at build time inside the closure — it keeps the rendered markup free of items the user can't reach:
if (Gate::allows('view-reports')) {
$menu->add('Reports', route('reports'));
}
Because the builder runs per request, this is safe but also the main performance consideration (see Gotchas).
Explicit active overrides for non-obvious pages. Pages reached from a parent nav entry (e.g. /products/42) usually won't light up the parent link. Use ->active() on the item, or widen active matching in config/menu.php so a prefix matches.
Localize titles at build time. $menu->add(__('nav.home'), route('home')) — titles are escaped on output, so passing translated strings is fine but passing HTML is not (see raw() below).
Compose presentation tweaks at the item level. ->icon('fa fa-home'), ->attributes(['class' => 'nav-item']) keep Bootstrap-class customisation out of your Blade templates; if you find yourself doing this repeatedly, write a small helper that maps "menu item spec" arrays to builder calls.
Testing. Menus are just state on the container; in a feature test assert on the rendered string:
$this->get('/')->assertSee('nav navbar-nav', false);
// or build directly:
$this->assertTrue(Menu::exists('navbar'));
navbar-nav items without the expected nav-item / nav-link classes and a broken dropdown toggle. Either override the presentational classes via item attributes plus a little CSS, or extend/replace the renderer. Budget an hour for this before committing to the package./en/about vs /about) and the home route (/) are all classic false-negative sources. Check config/menu.php first, then fall back to ->active() for special cases.Menu::make() call into a view composer that only runs for the layouts that render the menu, or memoize the expensive parts.raw() bypasses escaping. add() escapes the title; use raw() only for markup you fully control (icons, counters, badges). Never feed user input to it.Menu is a very generic alias; if another package or a legacy app class claims it, resolve via app('menu') or the fully-qualified facade instead.php artisan config:cache, edits to published menu config won't take effect until you re-cache. If active states behave "stale", this is usually why.composer.json, and if you hit a fatal on a modern Laravel (return types, container signatures), the fastest path is vendoring/forking src/Builder.php and src/Item.php — they're small. spatie/laravel-menu and lavary/laravel-menu are the fallbacks if you need maintained alternatives.Menu::get('navbar')->getItems() (or dd(Menu::get('navbar'))) gives you the built tree with resolved URLs and active flags — far faster than reasoning about the final HTML string. Menu::exists('navbar') guards against the "menu not defined in this request" blank-render case, which typically happens when a provider is conditionally registered or the view is rendered outside the HTTP stack.How can I help you explore Laravel packages today?