hammadzafar05/mobile-bottom-nav
Thumb-friendly mobile bottom navigation bar for Filament v4 and v5 panels. It pulls items from your existing Filament navigation and renders a fixed bottom bar on mobile, with dark mode, safe-area insets, badges, a More button and sidebar integration. No theme or CSS setup needed.
Install the package and register the plugin in the panel provider you want the bar on — there is no install command, no config file, and no asset build step:
composer require hammadzafar05/mobile-bottom-nav
use Hammadzafar05\MobileBottomNav\MobileBottomNav;
public function panel(Panel $panel): Panel
{
return $panel->plugins([
MobileBottomNav::make(),
]);
}
Where to look first:
app/Providers/Filament/*PanelProvider.php) — that is the entire configuration surface.vendor/hammadzafar05/mobile-bottom-nav/resources/views/ — the Blade template, which is where the fixed positioning, dark-mode classes, and safe-area inset live. Publish it only if you need to change markup.getNavigation() output — because the plugin reads that, not a separate list. If a page is missing from the bar, the problem is almost always in your page's navigationSort/navigationGroup/shouldRegisterNavigation(), not in the plugin.First use case: ship the zero-config default. MobileBottomNav::make() renders 2 navigation items plus a "More" button (fromNavigation() defaults to 3 total slots, including More), and the More button opens Filament's sidebar. Verify it in a browser at < 1024px width (device toolbar), and confirm the bar is absent at desktop width — that's the whole acceptance test for a first pass.
Do not treat this as "just a plugin install" in review: hideSidebarToggle(true) is the default, and it removes Filament's own topbar hamburger below 1024px. Confirm the sidebar is still reachable (it is, via More) before merging.
Start with auto-extraction, add custom items only when the registry can't express what you want. Auto-extraction derives from Filament's navigation, so canAccess(), shouldRegisterNavigation(), and tenant scoping propagate for free. Once you call ->items(), extraction is disabled entirely — you now own visibility, ordering, and active state for every slot.
Tune the slot count deliberately. The arithmetic is fromNavigation(n) = n total slots; with More enabled that's n - 1 nav items.
MobileBottomNav::make()->fromNavigation(5); // 4 nav items + More
MobileBottomNav::make()->fromNavigation(4)->moreButton(false); // 4 nav items, no More
Custom items for the handful of destinations that don't live in the navigation registry (an inbox, a profile screen, an external tool). Note that a custom item has no route binding — you must supply isActive() yourself or the active pill never lights up:
MobileBottomNav::make()->items([
MobileBottomNavItem::make('Home')
->icon('heroicon-o-home')
->activeIcon('heroicon-s-home')
->url('/admin')
->isActive(fn () => request()->is('admin')),
MobileBottomNavItem::make('Inbox')
->icon('heroicon-o-inbox')
->url('/admin/inbox')
->badge(fn () => Message::unread()->count(), 'danger')
->visible(fn () => auth()->user()?->can('viewAny', Message::class)),
MobileBottomNavItem::make('Profile')
->icon('heroicon-o-user')
->url('/admin/profile')
->isActive(fn () => request()->is('admin/profile*')),
]);
Use * wildcards in isActive() for section roots, and prefer the activeIcon-filled/solid variant so the current tab is distinguishable on a small screen.
Badge closures (supported since v1.2) are the right way to show counts — evaluated per render, so no stale cache. visible() closures are the right way to hide admin-only tabs from non-admins.
Multiple panels: configuration is per-plugin-instance, not global. If you have AdminPanelProvider and AppPanelProvider, register the plugin in each, with different fromNavigation()/items() per audience. There is no shared config file to centralize this, so consider a small helper method or a shared trait if the config grows.
When the bar conflicts with other bottom-of-body UI, change the injection point rather than the view:
MobileBottomNav::make()->renderHook(PanelsRenderHook::BODY_END);
Publishing views is the last resort, for branding (custom colors, a different active treatment, replacing icons with SVG sprites):
php artisan vendor:publish --tag="mobile-bottom-nav-views"
If mobile tables/actions/modals are also a problem, pair this with filament-mobile-preset (same author) rather than hand-rolling a theme — the bar intentionally does navigation only.
fromNavigation() counts the More button. fromNavigation(3) is not "three nav items" — it's two plus More. This is the single most common misconfiguration; always write the intended split in a comment next to the call.moreButton(false) and hideSidebarToggle(true) are a trap. Hiding the hamburger is only applied when the bar rendered and More is enabled, so the sidebar stays reachable — but set ->hideSidebarToggle(false) explicitly when you disable More. It documents intent and protects you if that guard ever changes.hideSidebarToggle() reaches outside the widget. It mutates the panel shell below 1024px. If your design system or a custom topbar relies on that hamburger's position, opt out. Reviewers should see this line in the diff.navigationSort within each group — matching what a user sees scanning the real sidebar. If the bar surfaces something unexpected, check its navigationGroup first; sorting globally by navigationSort used to cause exactly this and is still how many devs mentally model it.badge(fn () => ...count()) on a custom item is a query on every panel page load including desktop. Keep badge closures cheap (counters, cached values) or gate them on a mobile signal — and profile a page after adding three of them.items() turns off auto-extraction completely, including permission filtering. After switching, walk through each role (super admin, tenant admin, plain user) on a phone — items you forgot to wrap in visible() are now visible to everyone.position: fixed at BODY_END. Long tables and forms will run underneath it. Filament's own layout does not add bottom padding for you; if content is obscured, add padding/scroll-margin-bottom in your panel's CSS and include the env(safe-area-inset-bottom) value so notched devices don't clip tap targets.configureMobileNav(MobileBottomNav $nav): MobileBottomNav helper.vendor/ after upgrading the package.^4.0 || ^5.0, so your filament/filament version is your responsibility — the README recommends 4.11.5+ / 5.6.5+ because earlier patches in both majors carry published advisories, including an unauthenticated temp-file upload on auth pages. Run composer audit and pin in composer.json.Filament::getNavigation() in a tinker/route context first. Nine times out of ten the item isn't in the registry (shouldRegisterNavigation() === false, failed policy check, wrong panel, or cluster-scoped), and no plugin option will fix that.moreButtonLabel() is translatable by default. If you override it, pass a translation key rather than a hardcoded English string so the bar stays consistent with the rest of a localized panel.How can I help you explore Laravel packages today?