maatwebsite/laravel-sidebar
A Laravel package for building configurable sidebars and navigation menus. Define items, links, icons, and active states in PHP, then render clean Blade sidebars with minimal setup.
Install
composer require spartner/laravel-sidebar
Version 3.1.0 is the release to target: it is the first in the 3.x line with Laravel 11 support, and it is additive over 3.0.0 (no breaking changes called out in the changelog). In practice that means ^9.0|^10.0|^11.0 on the framework side and a modern PHP 8 requirement — check vendor/spartner/laravel-sidebar/composer.json before bumping if your app is still on Laravel 8.
The service provider and Blade/view namespace are registered through package auto-discovery, so there is nothing to add to config/app.php in a standard Laravel 11 skeleton. Publish the views (and any config) only when you need to change markup or defaults:
php artisan vendor:publish --provider="SpartnerNL\LaravelSidebar\LaravelSidebarServiceProvider"
Where to look first
vendor/spartner/laravel-sidebar/src — the abstract Sidebar base class, plus the Menu, Group and Item objects you compose. This is the whole API surface in four files; read Item first, since almost every day-to-day change is a method call on it (url(), route(), icon(), authorize(), addItem(), prepend()/append()).README.md in the package root — the canonical rendering snippet (@sidebar(...) directive / $sidebar->render() depending on version) and the exact namespaces used by the 3.x fork.First use case — one sidebar, one group, a handful of items
Create app/Sidebars/MainSidebar.php, extend the package's Sidebar, and build the menu. The fork renamed namespaces away from the abandoned Maatwebsite\Sidebar package, so copy the use statements from the installed source rather than from old blog posts:
namespace App\Sidebars;
use SpartnerNL\LaravelSidebar\Library\Core\Group;
use SpartnerNL\LaravelSidebar\Library\Core\Item;
use SpartnerNL\LaravelSidebar\Library\Core\Sidebar;
class MainSidebar extends Sidebar
{
public function getMenu()
{
$this->menu->group('main', function (Group $group) {
$group->item(__('Dashboard'), function (Item $item) {
$item->route('dashboard');
$item->icon('fas fa-home');
});
$group->item(__('Customers'), function (Item $item) {
$item->route('customers.index');
$item->icon('fas fa-users');
$item->authorize(auth()->user()->can('viewAny', Customer::class));
});
});
return $this->menu;
}
}
Then render it once in your authenticated layout so every page inherits the menu. That single class is the 20% of the package you will use 80% of the time; everything else (groups, sub-items, badges, custom views) hangs off it.
Let each module own its slice of the menu. Rather than one 400-line MainSidebar, register groups/items from the service provider of the feature they belong to, so removing a package removes its navigation. The sidebar is resolved from the container, so a provider can grab it and prepend()/append() to an existing group. This is the single most valuable pattern for keeping a growing admin menu maintainable.
Prefer route names over URLs. Use $item->route('customers.show', $customer) instead of $item->url('/customers/'.$id). Route-name items get active-state detection and URL generation for free, survive route prefix/domain changes, and don't break when you rename a URI. Reserve $item->url() for genuinely external links (docs, status page, marketing site).
Authorize in the menu definition, not the view. authorize() with a gate/policy result removes the item entirely for users who can't use it. Keeping the checks next to the item means a new permission is a one-line diff, and you never render a link that 403s. Use Gate::allows(...)/$user->can(...) consistently — mixing authorize() in some places and @can in the Blade view produces menus whose shape is impossible to reason about.
Model the hierarchy deliberately. Groups are section headers (and render as separate blocks); sub-items are the expandable second level. Anything deeper than two levels is almost always a UX problem, not a sidebar API problem — flatten it.
Translate at build time. __('Customers') inside the item callback keeps the sidebar locale-aware without touching the views, and the same strings feed your breadcrumbs.
Cache the structure, not the result. Building the menu on every request is cheap at 20 items and not cheap at 200 with per-item queries. If you cache, cache the unfiltered structure and apply authorization after retrieval — caching a menu that has already been filtered per user is a reliable way to show an admin's navigation to a regular user after a role change.
Test the sidebar class, not the HTML. Assert route names, active flags and visibility per user role by instantiating MainSidebar and walking getMenu(). One or two rendered-HTML snapshot tests catch markup regressions; asserting on <li> classes for every item does not.
Keep your own layout/CSS ownership. Publish the views and treat them as yours. The default markup is generic; almost every project restyles it, and once published you are in control of upgrades to that file.
Maatwebsite\Sidebar, plus signature changes) — budget a real migration for the use statements and method signatures across every sidebar class.composer why-not spartner/laravel-sidebar 3.1.0 after composer update rather than assuming Laravel 11 support landed.bootstrap/cache/packages.php has no entry for the package, someone has dont-discover configured or a stale cache. php artisan package:discover then php artisan config:clear is the fix; the symptom is a "target class [sidebar] does not exist"-style binding error.Customer::count() inside an item callback becomes a query on every page in the app. Move counts to a view composer or a cached service, and remember to invalidate on deploy.authorize(false), a policy that doesn't exist, or route('customers.index') when the route is admin.customers.index will hide or throw silently depending on context. Temporarily dump() inside the closure, or run php artisan route:list | grep customers, before touching the sidebar code.append()ing from a controller after the layout has started rendering won't show up. If ordering matters, control it in the sidebar class or via explicit prepend() in the relevant provider.vendor:publish, minor-version markup tweaks won't reach you — diff the package view against yours when you upgrade.config/ directory by default. If the package ships a config file you intend to tune, publish it explicitly; editing a file that doesn't exist after the upgrade is a common dead end.app/Sidebars (application code) rather than subclassing package internals so a future fork/swap stays cheap.How can I help you explore Laravel packages today?