caffeinated/menus
caffeinated/menus is a Laravel package for creating dynamic, database-driven menus. Build nested items, control ordering, link to routes or URLs, and render configurable menus in Blade with ease.
Install and confirm compatibility first. The package pins Laravel versions from a 2021 release, so on Laravel 9+/PHP 8.x check the constraint before anything else:
composer require caffeinated/menus
composer why-not caffeinated/menus # if resolution fails, inspect the constraint
Service provider and Menu facade are auto-discovered on Laravel 5.5+. If you're on an older or non-standard setup, register Caffeinated\Menus\MenusServiceProvider in config/app.php and alias the facade.
Where to look first. Read these three classes in vendor/caffeinated/menus/src before writing anything:
Menus.php — the manager/registry behind the facade (make, get, exists, render).Menu.php — the named menu container: add(), attribute helpers, render().Item.php — a single link: url, route, raw, attr, isActive, nesting via add().Then look at the package's Blade views (menu wrapper + item partial). Those two files define the markup and the class names you'll be targeting with CSS, so skim them before you start styling.
First use case — the primary nav. Define it once in a service provider or, better, a view composer so it's available to your layout:
use Caffeinated\Menus\Facades\Menu;
Menu::make('primary', function ($menu) {
$menu->add('Home', '/');
$menu->add('About', '/about');
$menu->add('Contact', '/contact');
});
Render it in layouts/app.blade.php:
{!! Menu::render('primary') !!}
That's the whole loop: define → store under a name → render by name. Every other feature is an extension of those three steps.
Build menus in a view composer, not a controller. Menus are shared across layouts, so bind them once:
View::composer('layouts.app', function ($view) {
if (! Menu::exists('primary')) {
Menu::make('primary', function ($menu) { /* ... */ });
}
});
Guard with Menu::exists() — the registry persists for the request, and a second make() on the same name will either overwrite or double up depending on version.
Nesting: capture the parent item and call add() on it. This is the cleanest, least magic-dependent form:
$about = $menu->add('About', '/about');
$about->add('Who We Are', '/about/who-we-are');
$about->add('What We Do', '/about/what-we-do');
$services = $menu->add('Services', '/services');
$services->add('Design', ['route' => 'services.design']);
$services->add('Development', ['route' => 'services.development']);
Prefer route names over hard-coded URLs for anything internal — it survives URL changes and makes active-state detection far more reliable.
Drive menus from config or the database, not literals. A config/navigation.php array of ['title', 'route', 'children' => [...], 'order' => 10] lets you sort by key and loop:
collect(config('navigation.primary'))
->sortBy('order')
->each(fn ($link) => $menu->add($link['title'], ['route' => $link['route']]));
This also gives you a cacheable representation (see Gotchas).
Attributes and presentation. Set them on the item, not in Blade:
$menu->add('Docs', ['route' => 'docs'])
->attr('class', 'nav-link')
->attr('target', '_blank');
Do class/id/data work at definition time so all markup decisions live in one file, and the Blade view stays a dumb recursive renderer.
Raw HTML for things that aren't links. Badges, dividers, a logout button with a CSRF-hidden form:
$menu->add('Divider', ['raw' => '<hr class="nav-divider">']);
// or
$menu->add('Notifications', ['raw' => view('partials.nav-badge', $data)->render()]);
Filter by authorization before adding. Menus leak admin links if you filter only in the view:
if (Gate::allows('view-reports')) {
$menu->add('Reports', ['route' => 'reports.index']);
}
Active states. Items expose an active check you can use in the default view, and typically a child-aware variant so a parent stays highlighted while a sub-page is open. Rely on that for active/open classes rather than comparing request()->url() by hand in Blade — the package already handles route-based matching.
Custom rendering. Publish or override the views under resources/views/vendor/caffeinated/menus/, keep the same variable names the package passes in, and recurse over children in the item partial. Add your own wrapper markup there instead of post-processing the output string.
The package is dormant (last release 2021). Expect friction on newer Laravel: removed str_*/array_* helpers, stricter facade auto-discovery, PHP 8.1 deprecations. Do a smoke test (define + nest + render) on your exact version before committing to it, and be prepared to fork and patch a small file rather than wait for an upstream fix.
Active detection is string/route based — be deliberate. Trailing slashes, query strings, locale prefixes, and wildcard routes (/blog/*) are the usual sources of "nothing is ever active" or "everything is active." Test with a URL that has a query string attached. If it misbehaves, stop fighting it: add an explicit attribute and compare route names yourself in the view.
raw() is an XSS hole if you feed it user data. Only pass strings you generated (rendered Blade partials, static markup). Never pass a menu title from the database through raw.
Build-once semantics. The registry holds the built menu for the request. Defining inside a loop over models, or in a partial that renders twice, silently duplicates items. Use Menu::exists() guards, or reset/rebuild explicitly when the menu is context-dependent (per-tenant, per-locale).
Closures don't cache. If you cache the navigation structure in Redis, cache the plain array of links and rebuild the Menu object from it per request. Serializing a menu built from closures will fail or produce garbage.
Order is insertion order. There's no magic sort unless you apply one; if the design demands a specific order (dividers between groups, CTA last), encode it in your data structure and sort before calling add(), otherwise a config reshuffle silently reorders the nav.
Facade name collisions. Menu is a popular alias — Spatie's menu package, various admin panels. If you get "class not found" or calls hitting the wrong object, fully qualify Caffeinated\Menus\Facades\Menu or inject the manager instead of aliasing.
Debugging: dump the built tree before blaming the view — dd(Menu::get('primary')->getItems()) shows exactly what was added, in what order, with what attributes. Nine times out of ten the bug is a menu defined on the wrong composer or a name typo (exists('primary') vs make('main')), not the renderer.
Extension points worth knowing: the published Blade views (markup/classes), the Item model-level helpers (add your own active logic by extending or wrapping), and the config array feeding make() (swap to a DB repository later without touching your layout).
How can I help you explore Laravel packages today?