spatie/laravel-menu
Build HTML menus in Laravel with a fluent API. Generate links via routes/actions/URLs, add classes and attributes, mark active items from the current request, and define reusable menu macros. Easy to render in Blade and customize output.
Installation:
composer require spatie/laravel-menu
Publish config (if needed) with:
php artisan vendor:publish --provider="Spatie\Menu\MenuServiceProvider"
First Menu:
Define a macro in a service provider (e.g., AppServiceProvider):
use Spatie\Menu\Menu;
use Spatie\Menu\MenuItem;
Menu::macro('main', function () {
return Menu::new()
->add('Home', 'home')
->add('About', 'about')
->add('Contact', 'contact')
->setActiveFromRequest();
});
Render in Blade:
<nav>
{!! Menu::main() !!}
</nav>
Create a simple top-bar menu with active state detection:
Menu::macro('topBar', function () {
return Menu::new()
->action('HomeController@index', 'Home')
->action('ProductsController@index', 'Products')
->action('CartController@index', 'Cart')
->setActiveFromRequest();
});
Blade Output:
<ul>
{!! Menu::topBar() !!}
</ul>
Nested Menus:
Menu::macro('admin', function () {
return Menu::new()
->add('Dashboard', route('admin.dashboard'))
->add('Users', route('admin.users.index'))
->add('Settings', route('admin.settings.index'))
->add('Reports')
->add('Sales', route('admin.reports.sales'))
->add('Analytics', route('admin.reports.analytics'));
});
Conditional Items:
Menu::macro('user', function () {
return Menu::new()
->addIfCan('Profile', 'view profile', route('profile.show'))
->addIfCan('Settings', 'edit settings', route('settings.edit'))
->add('Logout', route('logout'));
});
From Database:
use App\Models\NavigationItem;
Menu::macro('dynamic', function () {
return Menu::new()
->fromModel(NavigationItem::query(), 'title', 'url')
->setActiveFromRequest();
});
From API:
Menu::macro('apiMenu', function () {
$items = Http::get('https://api.example.com/navigation')->json();
return Menu::new()->fromCollection($items, 'name', 'href');
});
Leverage Laravel helpers:
Menu::macro('routes', function () {
return Menu::new()
->toRoute('home', 'Home')
->toRoute('products.index', 'Products')
->toAction([ProductController::class, 'index'], 'Products')
->toUrl('/contact', 'Contact');
});
Add Classes/Attributes:
Menu::macro('styled', function () {
return Menu::new()
->add('Home', route('home'))
->class('active')
->attr(['data-testid' => 'home-link']);
});
Blade Directives: Extend with custom directives:
// Register in AppServiceProvider
Blade::directive('menu', function ($expression) {
return "<?php echo \\Spatie\\Menu\\Menu::{$expression}(); ?>";
});
Usage:
<nav>
@menu('main')
</nav>
Cache menus for performance:
Menu::macro('cached', function () {
return cache()->remember('menu.main', now()->addHours(1), function () {
return Menu::new()
->add('Home', route('home'))
->add('About', route('about'));
});
});
Dynamic menus via middleware:
// app/Http/Middleware/SetMenu.php
public function handle(Request $request, Closure $next) {
$request->merge(['menu' => Menu::macro('user')]);
return $next($request);
}
Blade Access:
{!! $menu !!}
Active State Conflicts:
setActiveFromRequest() uses Laravel’s request()->is() method. Override with custom logic:
->setActiveFromRequest(function ($route) {
return $route->current() || request()->is('admin/*');
});
Nested Menu Indentation:
->setHtmlAttribute('class', 'dropdown-menu')
->setHtmlAttribute('style', 'margin-left: 20px;');
Macro Overwriting:
class MenuService {
public static function admin() { ... }
}
PHP 8+ Syntax:
addIfCan use callable syntax:
->addIfCan('Delete', fn() => auth()->user()->can('delete'), route('delete'));
Blade Escaping:
{{ !! Menu::macro() !! }} to escape HTML properly.Inspect Menu Structure:
dd(Menu::macro('main')->toHtml());
or dump the raw object:
dd(Menu::macro('main')->getItems());
Check Active State:
Menu::macro('main')->setActiveFromRequest();
dd(Menu::macro('main')->getActiveItem());
Validate URLs:
route() helpers to ensure URLs are resolvable:
if (! route('home')) {
throw new \RuntimeException('Route "home" not defined.');
}
Custom Menu Items:
Extend Spatie\Menu\MenuItem:
class CustomMenuItem extends MenuItem {
public function setBadge($text) {
$this->html .= "<span class='badge'>{$text}</span>";
return $this;
}
}
Usage:
Menu::macro('notifications', function () {
return Menu::new()->add(new CustomMenuItem('Alerts', '#'))
->setBadge('3');
});
Override Default HTML: Replace the default renderer:
Menu::macro('customRenderer', function () {
return Menu::new()
->add('Home', route('home'))
->setRenderer(function ($items) {
return "<div class='custom-menu'>" . implode('', $items) . "</div>";
});
});
Integrate with Laravel Breeze/Jetstream: Dynamically add auth-based items:
Menu::macro('authMenu', function () {
return Menu::new()
->addIf(auth()->check(), 'Profile', route('profile.show'))
->addIf(auth()->check(), 'Logout', route('logout'));
});
Avoid Over-Caching:
Menu::macro('cached', function () {
return cache()->rememberForever('menu.main', function () {
return Menu::new()->add('Home', route('home'));
});
});
Lazy-Load Heavy Menus:
Menu::macro('lazyMenu', function () {
return function () {
return Menu::new()->fromModel(NavigationItem::query());
};
});
Mock Menus in Tests:
Menu::macro('testMenu', function () {
return Menu::new()->add('Test', '#');
});
$this->assertStringContainsString('Test', Menu::testMenu()->toHtml());
Test Active States:
$menu = Menu::macro('testMenu')->setActiveFromRequest();
$this->assertEquals('active', $menu->getActiveItem()->getHtmlAttribute('class'));
Verify Conditional Items:
$this->actingAs($user)->get('/')->assertSee('Profile');
$this->actingAs($guest)->get('/')->assertDontSee('Profile');
How can I help you explore Laravel packages today?