konekt/menu
Konekt Menu is a Laravel package for building and rendering hierarchical menus with an expressive PHP API. It supports submenus, icons, permissions, active-state detection, and Blade rendering, making it easy to define navigation once and reuse it throughout your application.
konekt/menu is a small, framework-native menu builder: you register named menus and items in PHP, then render them in Blade. It is not a full navigation/CMS system — think "structured array of links with active-state detection," not an admin menu UI.
Install and confirm discovery:
composer require konekt/menu
Laravel 5.5+ auto-discovers Konekt\Menu\MenuServiceProvider; on older versions add it to config/app.php, and register the Menu facade if you want the static API.
Where to look first, in order:
README.md — the API surface is tiny enough that the readme is effectively the manual.src/Menu.php, src/Item.php, src/Link.php — these three files tell you everything about attributes, nesting, and active-state logic.src/MenuServiceProvider.php — what is bound in the container and whether anything is configurable/publishable.First use case: a top navbar. In AppServiceProvider::boot() (or a dedicated MenuServiceProvider):
use Konekt\Menu\Facades\Menu;
Menu::create('navbar');
Menu::get('navbar')->addItem('home', 'Home')->route('home');
Menu::get('navbar')->addItem('about', 'About')->url('/about');
Then in resources/views/layouts/app.blade.php:
{!! Menu::get('navbar')->render() !!}
That is the whole loop: define once at boot, render once in the layout. Start here before writing any custom markup — it validates that your service provider registration and active-state logic work.
Centralize registration, one provider. Put every menu definition in a single MenuServiceProvider rather than scattering addItem() calls across controllers. Controllers should never build menus; they should only choose which menu to render. This keeps definitions greppable and testable.
Route-based links over URLs. ->route('users.show', ['user' => $user->id]) beats ->url('/users/'.$user->id) — it survives route renames and gives the package a reliable hook for active detection. Reserve ->url() for external links and assets.
Nesting for dropdowns. Build the parent first, then attach children to the returned item:
$admin = Menu::get('navbar')->addItem('admin', 'Admin')->icon('fa fa-cog');
$admin->addSubItem('users', 'Users')->route('admin.users.index');
$admin->addSubItem('roles', 'Roles')->route('admin.roles.index');
The parent item becomes the dropdown toggle; use ->data('toggle', 'dropdown') and ->attribute('class', 'dropdown-toggle') to match Bootstrap, or drop the renderer entirely and iterate $menu->items yourself.
Conditional visibility belongs at registration. Gate items by permission at boot time, not in the view:
if (Auth::check() && Auth::user()->can('view-reports')) {
Menu::get('navbar')->addItem('reports', 'Reports')->route('reports.index');
}
Because menus are built per-request, this is cheap and avoids leaking link markup for unauthorized users.
Custom rendering. render() emits a conventional <ul>/<li> tree; when your design system needs different markup, loop the collection and use the accessors:
@foreach (Menu::get('sidebar')->items as $item)
<a href="{{ $item->url }}" class="{{ $item->isActive() ? 'active' : '' }}">
{{ $item->label }}
</a>
@endforeach
This is the single most useful escape hatch: you get the active-state logic and link resolution without the package's HTML.
Modular apps (Concord or similar). Each module registers its own items in its service provider's boot() against a shared menu name. Rely on insertion order for grouping — module boot order effectively determines menu order, so document that if ordering matters.
Testing. Assert on the menu object, not the rendered HTML: Menu::get('navbar')->hasItem('reports'), $item->isActive(), $item->url. HTML assertions break on every markup tweak.
create() vs get(). Creating a menu twice in one request can reset or duplicate items depending on version. Guard with a null check (if (!Menu::exists('navbar'))) or, better, register each menu exactly once in one provider. get() on an unregistered name throws — a common failure when a package or module forgets to create() first.
Active detection is match-based, not intent-based. It compares the current request against the item's resolved URL/route. Trailing slashes, query strings, locale prefixes, and index vs show routes all undermine it. For parent items that own a section, explicitly activate: $admin->activate() when the current route starts with admin.. Where the package supports it, prefer ->activateOnRoute('admin.*')-style patterns over URL guessing.
Lazy route resolution. ->route() resolves the URL when rendered. A route whose required parameter is missing (or model-bound to null) throws at render time in the layout — far from where the item was defined. Define items with route params already available, or wrap model-dependent items in an if.
Rendering is unescaped HTML. You must use {!! !!}. Corollary: any user-supplied string passed as a label or attribute is an XSS vector — escape labels before handing them to the builder, or render manually with {{ }}.
Ordering is insertion order only. There is no sort/priority attribute you can rely on. Reorder the registration calls, or collect items into your own array and sort before rendering.
Menus are rebuilt every request. Fine for a handful of items; if you generate hundreds (e.g. a full sitemap), cache the structure and re-evaluate only active state per request. Do not cache rendered HTML wholesale — active classes are user- and URL-specific.
Config and config:cache. If the package exposes a config file, publish it with vendor:publish and read config lazily inside the provider, not at file load. Anything resolved in a service provider's constructor can behave differently under php artisan config:cache.
Extension points. The menu and item classes are plain PHP; subclass Item to add accessors (e.g. badgeCount()), and check whether MenuFactory/container bindings let you swap the class. That is usually cleaner than post-processing rendered HTML with str_replace.
Know when not to use it. With ~28 stars and low activity, treat it as a thin utility you may need to read and patch. If your needs grow past "named list of links with an active flag," a 40-line Blade component backed by an array will be easier to maintain and debug than extending this package.
How can I help you explore Laravel packages today?