spatie/laravel-menu
Build HTML menus in Laravel with a fluent API. Add links via routes/actions/URLs, customize attributes and classes, and automatically set active items from the current request. Includes macros for reusable menu builders.
Installation:
composer require spatie/laravel-menu
Publish the config (if needed) with:
php artisan vendor:publish --provider="Spatie\Menu\MenuServiceProvider"
Define a Menu Macro:
Register a reusable menu in a service provider (e.g., AppServiceProvider):
use Spatie\Menu\Menu;
use Spatie\Menu\Laravel\Facades\Menu as MenuFacade;
Menu::macro('main', function () {
return Menu::new()
->action('HomeController@index', 'Home')
->action('AboutController@index', 'About')
->action('ContactController@index', 'Contact')
->setActiveFromRequest();
});
Render in Blade:
<nav>
{!! MenuFacade::main() !!}
</nav>
Create a header navigation menu for a blog:
// app/Providers/AppServiceProvider.php
Menu::macro('blogHeader', function () {
return Menu::new()
->url('/blog', 'Blog')
->action('PostController@index', 'Posts')
->url('/blog/tags', 'Tags')
->setActiveFromRequest();
});
Blade:
<header>
{!! Menu::blogHeader() !!}
</header>
Menu Composition:
url(), action(), html(), or view().
Menu::new()
->url('/dashboard', 'Dashboard')
->action('AdminController@index', 'Admin')
->html('<li>Static Item</li>', 'Static');
->add() with sub-menus:
Menu::new()
->add('Products')
->add('Services')
->add('Support')
->add('FAQ')
->add('Contact');
Dynamic Active States:
setActiveFromRequest() (matches current route/URL).setActive():
->action('ProfileController@edit', 'Profile')
->setActive(fn () => Auth::check());
Conditional Items:
addIfCan with Laravel Gates/Policies:
->addIfCan('Users', 'view users', 'Users')
->action('UserController@index', 'Manage Users');
urlIf, actionIf:
->urlIf(request()->has('promo'), '/promo', 'Promo');
Reusable Macros:
Menu::macro('footer', function () {
return Menu::new()
->url('/privacy', 'Privacy')
->url('/terms', 'Terms')
->url('/sitemap', 'Sitemap');
});
Blade Directives: Extend Blade with custom directives for menus:
Blade::directive('menu', function ($expression) {
return "<?php echo \\Spatie\\Menu\\Laravel\Facades\\Menu::{$expression}(); ?>";
});
Usage:
@menu('blogHeader')
Middleware: Dynamically modify menus based on user roles:
public function handle(Request $request, Closure $next) {
if (auth()->check()) {
Menu::macro('main', fn () => Menu::new()
->action('DashboardController@index', 'Dashboard')
->action('ProfileController@edit', 'Profile'));
}
return $next($request);
}
API-Driven Menus: Fetch menu items from a database:
Menu::macro('apiMenu', function () {
$items = MenuItem::where('active', true)->get();
return Menu::new()->items($items->map(fn ($item) =>
Menu::item()->url($item->path)->title($item->name)
));
});
Localization:
Menu::macro('i18nMenu', function () {
return Menu::new()
->url('/en', trans('menu.english'))
->url('/es', trans('menu.spanish'));
});
Active State Conflicts:
setActiveFromRequest() uses Laravel’s Request::is() under the hood. Overlapping routes (e.g., /posts and /posts/*) may cause unexpected active states.setActive() with custom logic or adjust route patterns.Macro Overwriting:
adminSidebar) to avoid collisions.URL Generation Issues:
action() and route() methods require valid Laravel route names/controllers. Invalid routes throw InvalidArgumentException.$this->get('/invalid')->expectException(\InvalidArgumentException::class);
Blade Escaping:
Menu::toHtml() escapes output by default. Use ->toHtml(false) for raw HTML (e.g., for JavaScript-generated menus).Caching Quirks:
Cache::remember('menu.main', now()->addHours(1), fn () => Menu::main());
dd(Menu::new()->url('/test', 'Test')->toHtml()) to debug the generated HTML structure.->setActive(true) to test items to verify logic.Custom Menu Items:
Extend the Spatie\Menu\MenuItem class for specialized items (e.g., dropdowns with icons):
class IconMenuItem extends MenuItem {
public function icon($icon): self {
$this->data['icon'] = $icon;
return $this;
}
}
Usage:
Menu::new()->add((new IconMenuItem())->url('/dashboard', 'Dashboard')->icon('fas fa-tachometer'));
Blade Components:
Replace toHtml() with a Blade component for complex rendering:
Menu::macro('componentMenu', function () {
return new \Spatie\Menu\MenuItemCollection([
Menu::item()->url('/home', 'Home')->setActive(true),
]);
});
Blade:
<x-menu :items="Menu::componentMenu()" />
Event Listeners:
Dynamically modify menus via events (e.g., Illuminate\Auth\Events\Login):
public function handle(Login $event) {
Menu::macro('userMenu', fn () => Menu::new()
->action('ProfileController@edit', 'Profile')
->action('LogoutController@store', 'Logout'));
}
Lazy-Loading: Defer menu building until needed:
Menu::macro('lazyMenu', function () {
return fn () => Menu::new()->url('/heavy', 'Heavy')->toHtml();
});
Blade:
{!! Menu::lazyMenu()() !!}
View Caching: Cache Blade views containing menus:
@cache(['menu', auth()->id()])
{!! Menu::main() !!}
@endcache
php artisan route:cache is run if using route-based menus to avoid runtime route resolution delays.MenuServiceProvider to avoid ClassNotFoundException.->action(
controller: 'PostController',
method: 'index',
title: 'Posts',
active: true
);
How can I help you explore Laravel packages today?