sysmatter / laravel-navigation
Configurable navigation package for Laravel with breadcrumbs and tree generation
Installs: 186
Dependents: 0
Suggesters: 0
Security: 0
Stars: 0
Watchers: 0
Forks: 1
Open Issues: 0
pkg:composer/sysmatter/laravel-navigation
Requires
- php: ^8.2
- illuminate/routing: *
- illuminate/support: *
Requires (Dev)
- captainhook/captainhook: ^5.25
- captainhook/plugin-composer: ^5.3
- larastan/larastan: *
- laravel/pint: *
- laravel/wayfinder: *
- orchestra/testbench: *
- pestphp/pest: ^3.0
- pestphp/pest-plugin-laravel: ^3.2
- phpstan/extension-installer: ^1.4
- phpstan/phpstan: ^2.1
- phpstan/phpstan-phpunit: ^2.0
- phpunit/phpunit: ^11.5
Suggests
- laravel/wayfinder: Optional integration for automatic route-based navigation
README
A powerful, flexible navigation management package for Laravel applications. Define multiple navigation structures with breadcrumbs, active state detection, and pre-compiled icons - perfect for React, Inertia.js, and traditional Blade applications.
Features
- πΊοΈ Multiple Navigations - Define unlimited navigation structures (main nav, footer, sidebars, etc.)
- π Route-Based - Use Laravel route names with full IDE autocomplete support
- π Breadcrumb Generation - Automatically generate breadcrumbs from your navigation structure
- π³ Tree Export - Export navigation as nested JSON for frontend frameworks
- β¨ Active State Detection - Smart detection of active menu items and their parents
- π¨ Pre-compiled Icons - Compile Lucide icons to SVG strings for optimal performance
- π Action Support - Define POST/DELETE actions (logout, form submissions, etc.)
- π External URLs - Mix internal routes with external links
- β Validation - Artisan command to validate all route references
- π§ͺ Fully Tested - Comprehensive Pest test suite
Installation
Install via Composer:
composer require sysmatter/laravel-navigation
Publish the configuration file:
php artisan vendor:publish --tag=navigation-config
Configuration
Define your navigations in config/navigation.php:
return [ 'navigations' => [ 'main' => [ [ 'label' => 'Dashboard', 'route' => 'dashboard', 'icon' => 'home', ], [ 'label' => 'Users', 'route' => 'users.index', 'icon' => 'users', 'can' => 'view-users', // Only show if user has permission 'children' => [ ['label' => 'All Users', 'route' => 'users.index'], ['label' => 'Roles', 'route' => 'users.roles.index', 'can' => 'manage-roles'], ['label' => 'Permissions', 'route' => 'users.permissions.index', 'can' => 'manage-permissions'], ], ], [ 'label' => 'Settings', 'route' => 'settings.index', 'icon' => 'settings', 'visible' => fn() => auth()->user()?->isAdmin(), // Dynamic visibility ], ], 'user_menu' => [ ['label' => 'Profile', 'route' => 'profile.edit', 'icon' => 'user'], ['label' => 'Settings', 'route' => 'settings.index', 'icon' => 'settings'], ['label' => 'Logout', 'route' => 'logout', 'method' => 'post', 'icon' => 'log-out'], ], 'footer' => [ ['label' => 'Documentation', 'url' => 'https://docs.example.com'], ['label' => 'Privacy Policy', 'route' => 'legal.privacy'], ['label' => 'Terms of Service', 'route' => 'legal.terms'], ], ], 'icons' => [ 'compiled_path' => storage_path('navigation/icons.php'), ], ];
Usage
Basic Usage
Get a navigation tree for your frontend:
use SysMatter\Navigation\Facades\Navigation; // Get navigation tree $mainNav = Navigation::get('main')->toTree(); // Pass to Inertia return inertia('Dashboard', [ 'navigation' => $mainNav, ]); // Or return as JSON return response()->json([ 'navigation' => $mainNav, ]);
With Route Parameters
For routes that require parameters:
// Route: /users/{user}/posts $navigation = Navigation::get('sidebar')->toTree([ 'user' => $user->id, ]);
Wildcard Parameters
Overview
Wildcard parameters allow you to include CRUD detail/edit pages in breadcrumbs and active states without cluttering your
navigation menu. This is particularly useful for pages like /users/123/edit that should show breadcrumbs and mark
parent items as active, but shouldn't appear in the navigation itself.
Basic Usage
Breadcrumb-Only Items
Use breadcrumbOnly: true to hide items from navigation while keeping them in breadcrumbs:
'navigations' => [ 'main' => [ [ 'label' => 'Users', 'route' => 'users.index', 'icon' => 'users', 'children' => [ [ 'label' => 'Edit User', 'route' => 'users.edit', 'breadcrumbOnly' => true, 'params' => ['user' => '*'], ], ], ], ], ],
When visiting /users/5/edit:
- Navigation: Only shows "Users" link
- Breadcrumbs: Shows "Users > Edit User"
- Active State: "Users" is marked as active
Wildcard Syntax
Single Wildcard Parameter
The '*' wildcard matches any parameter value:
[
'label' => 'View Order',
'route' => 'orders.show',
'breadcrumbOnly' => true,
'params' => ['order' => '*'],
]
Matches:
/orders/1β/orders/999β/orders/abc-123β
Multiple Wildcard Parameters
[
'label' => 'Edit Team Member',
'route' => 'organizations.teams.members.edit',
'breadcrumbOnly' => true,
'params' => [
'organization' => '*',
'team' => '*',
'member' => '*',
],
]
Mixed Wildcard and Exact Parameters
Combine wildcards with exact values to match specific scenarios:
[
'label' => 'Edit Admin User',
'route' => 'organizations.users.edit',
'breadcrumbOnly' => true,
'params' => [
'organization' => 5, // Only match organization 5
'user' => '*', // Any user
],
]
Matches:
/organizations/5/users/10/editβ/organizations/5/users/99/editβ/organizations/3/users/10/editβ
Dynamic Labels
Single Model Instance
Use closures to create dynamic labels based on route model binding:
[
'label' => fn($user) => "Edit: {$user->name}",
'route' => 'users.edit',
'breadcrumbOnly' => true,
'params' => ['user' => '*'],
]
Result: "Users > Edit: John Doe"
Multiple Models
When multiple models are present, the closure receives all parameters:
[
'label' => fn($params) => "Edit {$params['user']->name} in {$params['organization']->name}",
'route' => 'organizations.users.edit',
'breadcrumbOnly' => true,
'params' => [
'organization' => '*',
'user' => '*',
],
]
Result: "Organizations > Users > Edit Jane Smith in Acme Corp"
Static Labels
You can also use simple strings for labels:
[
'label' => 'Edit User',
'route' => 'users.edit',
'breadcrumbOnly' => true,
'params' => ['user' => '*'],
]
Result: "Users > Edit User"
Active State
Automatic Bubbling
Active state automatically bubbles up through the navigation hierarchy:
[
'label' => 'Admin',
'route' => 'admin.dashboard',
'children' => [
[
'label' => 'Users',
'route' => 'admin.users.index',
'children' => [
[
'label' => 'Edit User',
'route' => 'admin.users.edit',
'breadcrumbOnly' => true,
'params' => ['user' => '*'],
],
],
],
],
]
When on /admin/users/5/edit:
- "Admin" is active β
- "Users" is active β
- "Edit User" is in breadcrumbs but not in nav
Common Patterns
CRUD Resources
[
'label' => 'Products',
'route' => 'products.index',
'icon' => 'package',
'children' => [
[
'label' => 'Create Product',
'route' => 'products.create',
'breadcrumbOnly' => true,
],
[
'label' => fn($product) => $product->name,
'route' => 'products.show',
'breadcrumbOnly' => true,
'params' => ['product' => '*'],
],
[
'label' => fn($product) => "Edit: {$product->name}",
'route' => 'products.edit',
'breadcrumbOnly' => true,
'params' => ['product' => '*'],
],
],
]
Nested Resources
[
'label' => 'Organizations',
'route' => 'organizations.index',
'children' => [
[
'label' => fn($org) => $org->name,
'route' => 'organizations.show',
'breadcrumbOnly' => true,
'params' => ['organization' => '*'],
'children' => [
[
'label' => 'Team Members',
'route' => 'organizations.members.index',
'params' => ['organization' => '*'],
],
[
'label' => fn($params) => "Edit {$params['member']->name}",
'route' => 'organizations.members.edit',
'breadcrumbOnly' => true,
'params' => [
'organization' => '*',
'member' => '*',
],
],
],
],
],
]
Parameters Without Wildcards
You can also pass regular parameters for URL generation:
// Generate navigation with specific params $tree = Navigation::get('main')->toTree(['filter' => 'active']); // Generate breadcrumbs with params $breadcrumbs = Navigation::breadcrumbs('main', 'users.edit', ['user' => 5]);
Tips
-
Model Requirements: When using dynamic labels with models, ensure your models implement
getRouteKey()or__toString()for proper URL generation. -
Performance: Wildcard matching happens at runtime, so it's performant even with many breadcrumb-only items.
-
Specificity: More specific routes should come before more general ones in your navigation config.
-
Navigation vs Breadcrumbs: Use
breadcrumbOnlyfor detail/edit pages, andnavOnly(if needed) for items that should only appear in navigation but not breadcrumbs.
Frontend Integration
When building your React/TypeScript components, breadcrumb-only items won't appear in the navigation tree but will be present in breadcrumbs:
// Navigation returns only visible items const navTree = await fetch('/api/navigation/main'); // Items with breadcrumbOnly are excluded // Breadcrumbs include breadcrumb-only items const breadcrumbs = await fetch('/api/breadcrumbs'); // [ // { label: 'Users', url: '/users' }, // { label: 'Edit: John Doe', url: '/users/5/edit' } // ]
Breadcrumbs
Generate breadcrumbs from the current route:
// Auto-detect current route $breadcrumbs = Navigation::breadcrumbs(); // Specify nav tree to find current route in $breadcrumbs = Navigation::breadcrumbs('main'); // Or specify a route $breadcrumbs = Navigation::breadcrumbs('main', 'users.show'); // With parameters $breadcrumbs = Navigation::breadcrumbs('main', 'users.show', [ 'user' => $user->id, ]);
Output Format
The toTree() method returns an array structure perfect for frontend consumption:
[
[
'id' => 'nav-main-0',
'label' => 'Dashboard',
'url' => 'http://localhost/dashboard',
'isActive' => true,
'icon' => '<svg>...</svg>', // Compiled SVG (if icons compiled)
'children' => [],
],
[
'id' => 'nav-main-1',
'label' => 'Users',
'url' => 'http://localhost/users',
'isActive' => false,
'icon' => '<svg>...</svg>',
'children' => [
[
'id' => 'nav-main-1-0',
'label' => 'All Users',
'url' => 'http://localhost/users',
'isActive' => false,
'children' => [],
],
// ...
],
],
[
'id' => 'nav-main-2',
'label' => 'Logout',
'url' => 'http://localhost/logout',
'method' => 'post', // Only present when specified
'isActive' => false,
'icon' => '<svg>...</svg>',
'children' => [],
],
]
Frontend Integration
React + Inertia.js
import {Link} from '@inertiajs/react'; interface NavigationItem { id: string; type: 'link' | 'section' | 'separator'; label?: string; url?: string; method?: string; isActive?: boolean; icon?: string; children?: NavigationItem[]; } export default function Navigation({items}: { items: NavigationItem[] }) { return ( <nav> {items.map((item) => ( <div key={item.id}> {item.method ? ( <Link href={item.url} method={item.method} as="button" className={item.isActive ? 'active' : ''} > {item.icon && ( <span dangerouslySetInnerHTML={{__html: item.icon}}/> )} {item.label} </Link> ) : ( <Link href={item.url} className={item.isActive ? 'active' : ''}> {item.icon && ( <span dangerouslySetInnerHTML={{__html: item.icon}}/> )} {item.label} </Link> )} {item.children.length > 0 && ( <Navigation items={item.children}/> )} </div> ))} </nav> ); }
TypeScript + React
// Pass from Laravel const Page = ({navigation}: { navigation: NavigationItem[] }) => { return <Navigation items={navigation}/>; };
Breadcrumbs Component
import {Link} from '@inertiajs/react'; interface Breadcrumb { label: string; url: string; route: string; } export default function Breadcrumbs({items}: { items: Breadcrumb[] }) { return ( <nav aria-label="Breadcrumb"> <ol className="flex space-x-2"> {items.map((item, index) => ( <li key={item.route} className="flex items-center"> {index > 0 && <span className="mx-2">/</span>} {index === items.length - 1 ? ( <span className="text-gray-500">{item.label}</span> ) : ( <Link href={item.url} className="text-blue-600 hover:underline"> {item.label} </Link> )} </li> ))} </ol> </nav> ); }
Icon Compilation
For optimal performance, compile Lucide icons to SVG strings instead of using the DynamicIcon component:
php artisan navigation:compile-icons
This command:
- Extracts all icon names from your navigation config
- Downloads SVG files from the Lucide CDN
- Saves them as PHP arrays in
storage/navigation/icons.php - Automatically includes them in your navigation output
Benefits:
- β No runtime overhead
- β No client-side icon loading
- β Smaller bundle size
- β Faster page loads
Add to your deployment process:
php artisan navigation:compile-icons
Route Validation
Ensure all route names in your navigation config exist:
php artisan navigation:validate
Output:
Validating navigation: main
Validating navigation: user_menu
Validating navigation: footer
β All navigation routes are valid!
Or if there are errors:
Validating navigation: main
β Found 1 invalid route(s):
- main: Route 'users.invalid' not found (at: Users > Invalid Link)
Add to CI/CD:
# .github/workflows/tests.yml - name: Validate Navigation run: php artisan navigation:validate
Advanced Features
Custom Attributes
Add any custom data to navigation items:
'navigations' => [ 'main' => [ [ 'label' => 'Notifications', 'route' => 'notifications.index', 'badge' => '5', // Custom attribute 'badgeColor' => 'red', // Custom attribute 'requiresPro' => true, // Custom attribute ], ], ],
These will be included in the output:
[
'label' => 'Notifications',
'url' => 'http://localhost/notifications',
'badge' => '5',
'badgeColor' => 'red',
'requiresPro' => true,
// ...
]
Conditional Visibility
Control which navigation items are visible based on permissions, authentication, or custom logic.
Using visible Attribute
Show/hide items with boolean values or callables:
'navigations' => [ 'main' => [ // Static boolean [ 'label' => 'Beta Features', 'route' => 'beta.index', 'visible' => config('app.beta_enabled'), ], // Dynamic callable [ 'label' => 'Admin Panel', 'route' => 'admin.index', 'visible' => fn() => auth()->user()?->isAdmin(), ], // Complex logic [ 'label' => 'Premium Features', 'route' => 'premium.index', 'visible' => fn() => auth()->check() && auth()->user()->hasActiveSubscription(), ], ], ],
Using can Attribute (Authorization)
Leverage Laravel's authorization gates and policies:
'navigations' => [ 'main' => [ // Simple gate check [ 'label' => 'Users', 'route' => 'users.index', 'can' => 'view-users', ], // Policy with model [ 'label' => 'Edit Post', 'route' => 'posts.edit', 'can' => ['update', $post], // Checks: $user->can('update', $post) ], // Children inherit permissions [ 'label' => 'Admin', 'route' => 'admin.index', 'can' => 'access-admin', 'children' => [ ['label' => 'Users', 'route' => 'admin.users', 'can' => 'manage-users'], ['label' => 'Settings', 'route' => 'admin.settings'], ], ], ], ],
Combining Conditions
Use both visible and can together:
[
'label' => 'Billing',
'route' => 'billing.index',
'visible' => fn() => config('features.billing_enabled'),
'can' => 'view-billing',
]
Conditional Children
Filter child items independently:
[
'label' => 'Reports',
'route' => 'reports.index',
'children' => [
[
'label' => 'Sales Report',
'route' => 'reports.sales',
'can' => 'view-sales',
],
[
'label' => 'Financial Report',
'route' => 'reports.financial',
'can' => 'view-financials',
],
[
'label' => 'Admin Report',
'route' => 'reports.admin',
'visible' => fn() => auth()->user()?->isAdmin(),
],
],
]
Note: If a parent has no visible children, the parent is still shown. If you want to hide the parent when all
children are hidden, add a visible check to the parent too.
Define Gates in AuthServiceProvider
// app/Providers/AuthServiceProvider.php use Illuminate\Support\Facades\Gate; public function boot(): void { Gate::define('view-users', function ($user) { return $user->hasPermission('view-users'); }); Gate::define('access-admin', function ($user) { return $user->isAdmin(); }); }
Benefits
β
Security - Items requiring permissions won't appear in navigation
β
Clean UI - Users only see what they can access
β
DRY - Reuse existing gates and policies
β
Flexible - Mix static config with dynamic logic
β
Type Safe - All authorization goes through Laravel's auth system
Active State Detection
The package intelligently detects active states:
- Exact match: If the current route is
users.index, that item is active - Parent match: If current route is
users.show, the parentusers.indexis also marked active - Child match: If current route is
users.roles.index, bothUsersparent andRoleschild are active
External URLs
Mix internal routes with external links:
[
'label' => 'API Docs',
'url' => 'https://api.example.com/docs',
'icon' => 'book-open',
],
Action Items
Define items that trigger POST/DELETE requests:
[
'label' => 'Logout',
'route' => 'logout',
'method' => 'post',
'icon' => 'log-out',
],
Sections and Separators
Organize your navigation with visual sections (containers) and dividers:
'navigations' => [ 'main' => [ ['label' => 'Dashboard', 'route' => 'dashboard', 'icon' => 'home'], // Section container with grouped items [ 'label' => 'Management', 'type' => 'section', 'children' => [ ['label' => 'Users', 'route' => 'users.index', 'icon' => 'users'], ['label' => 'Teams', 'route' => 'teams.index', 'icon' => 'users-2'], ], ], // Visual separator ['type' => 'separator'], ['label' => 'Settings', 'route' => 'settings.index', 'icon' => 'settings'], ['label' => 'Logout', 'route' => 'logout', 'method' => 'post', 'icon' => 'log-out'], ], ],
Section Features:
- Container Structure: Sections contain their children, making boundaries clear
- Permission Control: Apply
canorvisibleto entire section to hide all children - Nested Sections: Sections can contain other sections
- Auto-hiding: Sections with no visible children are automatically excluded
- Breadcrumb Exclusion: Sections don't appear in breadcrumbs, only their children do
Example with Permissions:
[
'label' => 'Admin',
'type' => 'section',
'can' => 'access-admin', // Hides entire section if user lacks permission
'children' => [
['label' => 'Dashboard', 'route' => 'admin.dashboard', 'icon' => 'layout-dashboard'],
['label' => 'Users', 'route' => 'admin.users.index', 'icon' => 'users'],
['label' => 'Edit User', 'route' => 'admin.users.edit', 'breadcrumbOnly' => true],
['label' => 'Roles', 'route' => 'admin.roles.index', 'icon' => 'shield'],
],
],
Types:
'link'- Regular navigation item (default)'section'- Container for grouping related items'separator'- Visual divider between items
React Example:
{ navigation.map((item) => { if (item.type === 'section') { return ( <div key={item.id} className="nav-section"> <h3 className="section-header">{item.label}</h3> <div className="section-content"> <Navigation items={item.children || []}/> </div> </div> ); } if (item.type === 'separator') { return <hr key={item.id} className="nav-separator"/>; } // Regular link rendering return <Link key={item.id} href={item.url}>{item.label}</Link>; }) }
Sections and separators are automatically excluded from breadcrumbs. The output includes a children array for
sections:
[
['id' => 'nav-main-0', 'type' => 'link', 'label' => 'Dashboard', ...],
[
'id' => 'nav-main-1',
'type' => 'section',
'label' => 'Management',
'children' => [
['id' => 'nav-main-1-0', 'type' => 'link', 'label' => 'Users', ...],
['id' => 'nav-main-1-1', 'type' => 'link', 'label' => 'Teams', ...],
],
],
['id' => 'nav-main-2', 'type' => 'separator'],
// ...
]
Navigation and Breadcrumb Visibility Control
Control where navigation items appear with navOnly and breadcrumbOnly:
'navigations' => [ 'main' => [ [ 'label' => 'Dashboard', 'route' => 'dashboard', 'children' => [ ['label' => 'Overview', 'route' => 'dashboard.overview'], // Hidden from main nav, only appears in breadcrumbs [ 'label' => 'Edit Dashboard', 'route' => 'dashboard.edit', 'breadcrumbOnly' => true, ], ], ], // Shown in nav but excluded from breadcrumb trail [ 'label' => 'Admin Section', 'route' => 'admin.index', 'navOnly' => true, 'children' => [ ['label' => 'Users', 'route' => 'admin.users'], ['label' => 'Settings', 'route' => 'admin.settings'], ], ], ], ],
Use Cases:
breadcrumbOnly- Perfect for edit/create pages that shouldn't clutter your main navigationnavOnly- Ideal for grouping/section items that provide structure in navigation but would be redundant in breadcrumbs
Example: Edit Pages
[
'label' => 'Users',
'route' => 'users.index',
'children' => [
['label' => 'All Users', 'route' => 'users.index'],
['label' => 'Edit User', 'route' => 'users.edit', 'breadcrumbOnly' => true],
['label' => 'Create User', 'route' => 'users.create', 'breadcrumbOnly' => true],
],
]
The navigation will show only "All Users", but breadcrumbs will display:
Users > Edit User
Example: Navigation Sections
[
'label' => 'Administration',
'route' => 'admin.index',
'navOnly' => true, // Don't include in breadcrumbs
'children' => [
['label' => 'Manage Users', 'route' => 'admin.users'],
],
]
Breadcrumbs will show only "Manage Users" without the redundant "Administration" parent.
Middleware Integration
Share navigation with all Inertia requests:
// app/Http/Middleware/HandleInertiaRequests.php use SysMatter\Navigation\Facades\Navigation; public function share(Request $request): array { return array_merge(parent::share($request), [ 'navigation' => [ 'main' => Navigation::get('main')->toTree(), 'user' => Navigation::get('user_menu')->toTree(), 'footer' => Navigation::get('footer')->toTree(), ], 'breadcrumbs' => Navigation::breadcrumbs('main'), ]); }
Testing
Run the test suite:
./vendor/bin/pest
With coverage:
./vendor/bin/pest --coverage
Configuration Reference
Navigation Item Options
| Option | Type | Description |
|---|---|---|
type |
string | Item type: 'link', 'section', or 'separator' |
label |
string | Display text for the item |
route |
string | Laravel route name (e.g., users.index) |
url |
string | External URL (alternative to route) |
method |
string | HTTP method for actions (post, delete, etc.) |
icon |
string | Lucide icon name (e.g., home, users) |
children |
array | Nested navigation items |
visible |
bool|callable | Controls visibility (static or dynamic) |
can |
string|array | Gate/policy check ('ability' or ['ability', $model]) |
breadcrumbOnly |
bool | Only show in breadcrumbs, hide from navigation |
navOnly |
bool | Only show in navigation, hide from breadcrumbs |
| custom | mixed | Any custom attributes you want to include |
Config Options
| Option | Default | Description |
|---|---|---|
navigations |
[] |
Array of named navigation structures |
icons.compiled_path |
storage/navigation/icons.php |
Where to save compiled icons |
Requirements
- PHP 8.2+, 8.3+, 8.4+
- Laravel 11.0+ or 12.0+
Contributing
Please see CONTRIBUTING for details.
Security
Please review our security policy for reporting vulnerabilities.
Credits
License
MIT License. See LICENSE file for details.