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

Filament Popup Laravel Package

zpmlabs/filament-popup

Filament Popup lets you create and manage modal popups directly from your Filament admin panel. Configure content, display conditions, and scheduling through a friendly resource, then show targeted messages to users without custom frontend code.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and register

composer require zpmlabs/filament-popup
php artisan vendor:publish --tag="filament-popup-migrations"
php artisan vendor:publish --tag="filament-popup-config"
php artisan migrate

Register the plugin on the panel(s) that should display popups — not globally in config/app.php:

// app/Providers/Filament/AdminPanelProvider.php
public function panel(Panel $panel): Panel
{
    return $panel
        ->plugin(FilamentPopupPlugin::make())
        // ...
}

Then run php artisan filament:assets (and php artisan optimize:clear if you're on a cached config) so the plugin's CSS/JS lands in public/.

Where to look first

  1. config/filament-popup.php — this is where the real behaviour lives: frequency/dismissal strategy, delay, cache TTL, whether the resource is registered, and the render hook the modal is injected into. Read this before writing any code.
  2. src/FilamentPopupPlugin.php — confirms which render hook (PanelsRenderHook::BODY_END or similar) is used, how resources/pages are registered, and what ->navigation...() / ->resource(...) style fluent methods exist.
  3. The generated PopupResource — it's the editing UI for popup records; its form schema tells you exactly which columns exist (title, body, image, CTA label/URL, is_active, publish window).
  4. The Blade view (publish with --tag="filament-popup-views" if you need to override) — this is where the Alpine component and dismissal storage key are defined.

First use case

A "what's new" announcement on the dashboard for logged-in users: create one popup record in the admin resource, set it active, point it at the dashboard, and give it a "Got it" dismiss that sticks per-user. That exercises the whole path — record → visibility rules → render hook → dismissal — in about five minutes, and it's the lowest-risk way to confirm the plugin is wired correctly on your panel before you start targeting.

Implementation Patterns

Content lives in the database, not in code. Treat popup records as content: seed them in a seeder for local/staging so you're not clicking through the admin every time you migrate:fresh. Keep a PopupSeeder alongside your UserSeeder and gate it on app()->environment('local').

Targeting is the whole game. The naive install shows the popup everywhere. The patterns that actually hold up:

  • Page scoping — restrict to specific routes/pages (dashboard, a resource index, a custom page) rather than "all authenticated pages". A modal on every page is the fastest way to get the plugin uninstalled.
  • Role/segment scoping — wrap visible popups in a query scope keyed off the current user (plan tier, role, onboarding-complete flag). If the plugin exposes a ->modifyQueryUsing()-style hook, use it; otherwise an Eloquent global scope on the model is the clean fallback.
  • Publish windows — use starts_at / ends_at for scheduled announcements instead of manually toggling is_active. You will forget to turn it off at 9am on Monday.
  • One-at-a-time — if the plugin renders multiple records, enforce ordering and a limit in your query scope. Stacked modals are a bug, not a feature.

Dismissal semantics — pick one and be consistent.

Strategy Implementation Good for
Per user, permanent row in a popup_dismissals pivot or JSON column feature announcements
Per session session key "we've updated the dashboard"
Time-based cookie with expiry promotions, trials
Per popup version key on popup_id + updated_at re-show after you edit the copy

If the package ships cookie/localStorage-only dismissal, that's a real limitation: dismissal doesn't follow the user across devices and doesn't survive private browsing. For anything product-critical, add a dismissals relationship on User and a small ->visible(fn () => ! auth()->user()->hasDismissed($popup)) style constraint.

Multi-tenancy. The migration almost certainly ships without a tenant_id. If you're on Filament tenancy, add the column in your own migration and scope it in the resource's getEloquentQuery() — otherwise tenant A's admin sees tenant B's popups in the list.

Triggering popups from code, not from the DB. For "show a welcome modal right after registration" or "confirm before leaving a dirty form", don't create a popup record — use Filament's own Notification/Action modal or dispatch a browser event and let a small Alpine listener open the plugin's modal component directly. Database-backed popups are for content editors; programmatic modals are for developers. Mixing the two produces records named test123 in production.

Resource hardening. The popup resource is a CMS surface. Add a policy (PopupPolicy) so only admins create/edit, and consider hiding it from the nav (->navigationGroup(...) + shouldRegisterNavigation()) if content folks live in a separate panel.

Testing. In phpunit.xml, point FILAMENT_POPUP_ENABLED=false (or whatever the config toggle is) so popups don't randomly appear in Dusk/browser tests. For unit tests, assertDatabaseHas('popups', ...) after seeding, and use Livewire::test(PopupResource\Pages\CreatePopup::class) for the admin path.

Gotchas and Tips

Publish migrations before migrate, always. If you install into an existing project and run php artisan migrate first, Laravel may prompt for the package migration while your app migrations are half-applied. vendor:publish → review the migration → migrate, in that order.

Config caching bites on deploy. php artisan config:cache snapshots filament-popup.php. If you flip the plugin on for one environment via env vars, you need config:cache in your deploy script after the deploy, or you'll get a stale value. Same for view:cache if you override the Blade view.

SPA mode (wire:navigate) is the most likely breakage. With Filament's SPA mode enabled, the page body isn't fully re-rendered on navigation, so a popup injected at BODY_END may only ever evaluate on hard loads. Test explicitly: navigate between two pages and see whether the popup re-fires. Fix by hooking livewire:navigated in a small Alpine listener that checks dismissal state, rather than relying on a server render.

z-index and focus trapping. Filament modals sit at a high z-index; a popup with a hardcoded z-50 will appear behind an action modal and in front of notifications. If the package's view uses a fixed z-index, override the view and bump it. Also verify Escape closes it and focus is trapped — cheap to check, embarrassing to ship otherwise.

Dismissal keys must be versioned. If the storage key is just popup-{id}, editing a popup's copy won't re-show it to anyone who already dismissed it. Key on id + a version column you bump intentionally.

Editing a popup doesn't invalidate caches. If the config has a cache TTL (common in these plugins), a content editor will save and see nothing change for up to an hour. Add a model observer on saved/deleted that calls Cache::forget(...) with the package's cache key — or set the TTL to 0 in non-production.

Rendering cost on public panels. If you enable the plugin on a guest-facing panel, the popup query runs on every request including login. Gate it behind auth()->check() in the plugin's canView/visible callback so anonymous traffic doesn't pay for it.

Accessibility is usually the weakest part of small popup packages. If the shipped markup is a bare div with x-show, you'll want to add role="dialog", aria-modal="true", aria-labelledby pointing at the heading, and x-trap from Alpine Focus. That's a 10-line view override and worth doing before any compliance review.

Extension points worth knowing: the published view (full markup control), the plugin's fluent setters (usually ->delay(), ->frequency(), ->pages()), and the config file. If a requirement doesn't fit any of those three, resist forking — a custom Livewire component using Filament's own modal is cleaner than maintaining a fork of a 9-star package.

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.
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode
jjsquady/round-robin