πŸ’‘ Custom Regulatory Compliance Dashboard See Demo

Create Custom Navigation in Filament (Grouped vs Ungrouped Menus + RBAC)

| 5 min read
James Manager

James Manager

Create Custom Navigation in Filament (Grouped vs Ungrouped Menus + RBAC)

Filament PHP provides an excellent admin panel framework with automatic navigation generation. However, sometimes you need more control - especially when you want to position navigation groups alongside standalone items without sacrificing access control.

This guide walks through building a custom navigation system in Filament v4 while maintaining proper role-based visibility.

🚧 The Challenge

Filament applies a strict and predictable ordering algorithm:

  1. Sort navigation items using navigationSort.
  2. Group them β€” items without groups automatically form an β€œungrouped” section.
  3. Ungrouped items are always placed first, before grouped items.

This creates an issue when you want an order like:

  • Dashboard (ungrouped)
  • Requisitions (group)
  • Inventory (group)
  • Users (ungrouped)
  • Settings (ungrouped)

↳ Filament will always place Dashboard, Users, and Settings before grouped items. You cannot mix them in between by default.

The Strategy: Custom Navigation Builder

Filament exposes a navigation() method in your PanelProvider, allowing full control of ordering. This means we can use anonymous groups (groups without labels) to position navigation stacks anywhere.

Step 1 β€” Define Custom Navigation

->navigation(function (NavigationBuilder $builder): NavigationBuilder {
    return $builder
        ->groups([
            NavigationGroup::make() // Dashboard
                ->items([
                    NavigationItem::make('Dashboard')
                        ->icon('heroicon-o-home')
                        ->isActiveWhen(fn (): bool => request()->routeIs('filament.app.pages.dashboard'))
                        ->url(fn (): string => Dashboard::getUrl()),
                ]),

            NavigationGroup::make('Requisitions')
                ->icon('heroicon-o-document-text')
                ->items([
                    ...RequisitionResource::getNavigationItems(),
                    ...InternalRequisitionResource::getNavigationItems(),
                ]),

            NavigationGroup::make('Inventory')
                ->icon('heroicon-o-cube')
                ->items([
                    ...InventoryItemResource::getNavigationItems(),
                    ...InventoryReturnResource::getNavigationItems(),
                    ...InventoryTransactionResource::getNavigationItems(),
                ]),

            NavigationGroup::make() // Users + Settings
                ->items([
                    ...UserResource::getNavigationItems(),
                    ...Settings::getNavigationItems(),
                ]),
        ]);
});

❗ Problem β€” Permissions Break

When you manually call getNavigationItems(), you bypass:

βœ… shouldRegisterNavigation() β€” controls visibility
Β βœ… canAccess() β€” controls page access

Meaning:

The menu item will render even if the user should not see it.

Example:

NavigationGroup::make()
    ->items([
        ...UserResource::getNavigationItems(),   // ❌ always visible
        ...Settings::getNavigationItems(),       // ❌ always visible
    ])

Even if:

public static function shouldRegisterNavigation(): bool
{
    return auth()->user()->hasRole('admin');
}

…it will still show.
Β So we must manually enforce permission checks.

βœ… Full Solution β€” Conditional Navigation Items

->navigation(function (NavigationBuilder $builder): NavigationBuilder {
    $groups = [
        /* … regular groups … */
    ];

    // Admin-only section
    $adminItems = [];

    if (UserResource::shouldRegisterNavigation()) {
        $adminItems = array_merge($adminItems, UserResource::getNavigationItems());
    }

    if (Settings::shouldRegisterNavigation()) {
        $adminItems = array_merge($adminItems, Settings::getNavigationItems());
    }

    if (count($adminItems) > 0) {
        $groups[] = NavigationGroup::make()->items($adminItems);
    }

    return $builder->groups($groups);
});

πŸ” Permissions Setup

Example β€” Users Resource

class UserResource extends Resource
{
    public static function shouldRegisterNavigation(): bool
    {
        return auth()->user()->hasRole('admin');
    }

    public static function canAccess(): bool
    {
        return auth()->user()->hasRole('admin');
    }
}