Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Mobile Bottom Nav Laravel Package

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.

View on GitHub
Deep Wiki
Context7

Getting Started

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:

  1. Your panel provider (app/Providers/Filament/*PanelProvider.php) — that is the entire configuration surface.
  2. 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.
  3. Filament's own 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.

Implementation Patterns

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.

Gotchas and Tips

  • 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.
  • Ordering follows the sidebar, not your intuition (since v1.3). Items come out in group order, then 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.
  • Desktop requests still execute your closures. The bar is viewport-hidden, not server-side gated. A 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.
  • The bar is 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.
  • Watch z-index against Filament modals and slide-overs, which also portal near the end of the body. Test opening a modal from a page while the bar is visible — if the bar floats above the modal backdrop, adjust the published view's z-index rather than the hook.
  • No config file means no upgrades to forget — and duplication to manage. Per-panel fluent config is simple but repeats across panels; if you need five panels consistent, extract a configureMobileNav(MobileBottomNav $nav): MobileBottomNav helper.
  • Published views drift. Once published, composer updates won't touch your copy. Pin a note in the PR and re-diff against vendor/ after upgrading the package.
  • The package deliberately does not pin a Filament floor (v1.4.1). Constraint stays ^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.
  • Debugging a missing tab: dump 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.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
roundly-consulting/kubernetes-api-for-laravel
roundly-consulting/plausible-for-laravel
gvp/gvp-laravel-template
metasyncsite/laravel-metasync-client
roundly-consulting/onboarding-for-laravel
andydefer/laravel-notification
adachsoft/cli-process-tester
jhaskell13/laraflow
sebastienheyd/boilerplate-media-manager
ccast/tagixo-primix
pretendtrue/laravel-builder
sawmainek/laravel-scout-tntsearch-driver
tomshaw/laravel-calendar-table
dibakar/laravel-ownership
steadrun/laravel-monitor
labyrinthe/connect-laravel
elmogy/layan
drewdan/sentinel-client
dskripchenko/laravel-llm
fatryst/nuonuo