An elegant, framework-agnostic Laravel package for managing notifications with Livewire, featuring per-user preferences, full dark mode support, zero-build self-contained assets, and a robust delivery pipeline.
The package ships its own self-contained CSS (prefixed nb-), served directly from vendor/ through a package route. No Tailwind, no Bootstrap, no npm run build, no vendor:publish needed — install via Composer, drop the component in your layout, done. It looks right in any project: Tailwind, Bootstrap, or plain CSS.
Each user manages their own experience through a preferences panel inside the bell:
dedup_key within a time window is droppedscheduled_at / expires_at respected automaticallygroup_key collapse into one expandable itemNotificationCreated, NotificationRead, NotificationArchived, NotificationDeletedcomposer require caiquebispo/notification-bell
php artisan migrate
Add the trait to your User model:
use CaiqueBispo\NotificationBell\Traits\HasNotifications;
class User extends Authenticatable
{
use HasNotifications;
}
Include the component in your layout (Livewire 3 bundles Alpine.js — nothing else to install):
@auth
<livewire:notification-bell />
@endauth
@livewireScripts
That's it. The component injects its own stylesheet automatically. If you prefer loading it in the <head>:
<head>
@notificationBellStyles
</head>
Publishing is optional — only for deep customization:
php artisan vendor:publish --tag="notification-bell-config" # config/notifications.php
php artisan vendor:publish --tag="notification-bell-views" # Blade views
php artisan vendor:publish --tag="notification-bell-lang" # translations
php artisan vendor:publish --tag="notification-bell-migrations" # migrations
Customize the look through config — applied via CSS variables, no build step:
'theme' => [
'mode' => 'auto', // auto | system | dark | light (see below)
'primary' => '#8b5cf6', // any CSS color, or names like 'violet'
'badge_background' => '#ef4444',
'badge_text' => '#ffffff',
'badge_style' => 'count', // count | dot | pulse
'badge_position' => 'top-right', // top-right | top-left | bottom-right | bottom-left
'radius' => '0.75rem',
'dropdown_width' => '22rem',
'toast_position' => 'bottom-right',// + top-left, top-center, bottom-center...
'bell_icon' => null, // Blade view with your own SVG
'item_view' => null, // Blade view to render each list item
],
Type colors come from the types section, so custom types get proper colors automatically:
'types' => [
'info' => ['color' => 'blue', 'icon' => 'info-circle'],
'success' => ['color' => 'green', 'icon' => 'check-circle'],
'warning' => ['color' => 'yellow', 'icon' => 'exclamation-triangle'],
'error' => ['color' => 'red', 'icon' => 'x-circle'],
'billing' => ['color' => '#8b5cf6', 'icon' => 'bell'], // custom type
],
Dark mode (theme.mode):
auto (default) — follows the host page's explicit markers only: .dark, [data-theme="dark"], or [data-bs-theme="dark"] (Bootstrap). A light site stays light even for users whose OS is in dark mode.system — additionally follows the OS prefers-color-scheme when no marker is present.dark / light — force one look regardless of the host.<livewire:notification-bell :limit="5" :polling="false" />
Enabled by default. Users open the gear icon inside the bell to control toasts, sound, volume, muted categories, snooze and quiet hours. Persisted in the notification_preferences table.
'preferences' => [
'enabled' => true,
'allow_sound_control' => true,
'allow_toast_control' => true,
'allow_category_control' => true,
'allow_snooze' => true,
'allow_quiet_hours' => true,
'snooze_options' => ['1h' => 60, '4h' => 240, '8h' => 480, '24h' => 1440],
],
Programmatic access:
$prefs = $user->bellPreferences();
$prefs->muteCategory('marketing');
$prefs->snoozeFor(120);
$prefs->isSnoozed(); // true
Group notifications above the type level. Users can mute categories they don't care about:
'categories' => [
'orders' => ['label' => 'Pedidos'],
'system' => ['label' => 'Sistema'],
'marketing' => ['label' => 'Novidades'],
],
NotificationHelper::info($userId, 'Sale!', '50% off today', null, null, [
'category' => 'marketing',
]);
A notification sent to a user who muted its category is silently skipped.
use CaiqueBispo\NotificationBell\Helpers\NotificationHelper;
// Simple
NotificationHelper::info($userId, 'Title', 'Message');
NotificationHelper::success($userId, 'Order confirmed', 'Order #123 confirmed!', ['order_id' => 123], route('orders.show', 123));
// Multiple users
NotificationHelper::create([1, 2, 3], 'Maintenance', 'Scheduled for tonight', 'warning');
// Full options
NotificationHelper::create($userId, 'New comment', 'Someone replied', 'info', null, '/posts/1', [
'category' => 'social',
'group_key' => 'post-1-comments', // groups with similar notifications
'dedup_key' => 'comment-42', // duplicate within window is dropped
'image_url' => '/avatars/maria.png',
'scheduled_at' => now()->addHour(), // deliver later
'expires_at' => now()->addDays(3), // disappears after
'queue' => false, // bypass the queue, create synchronously
]);
// Via the User trait
auth()->user()->success($userId, 'Done!', 'Operation completed.');
NotificationHelper::info($userId, 'Approval needed', 'A document awaits review', [
'actions' => [
['label' => 'Approve', 'url' => '/docs/1/approve', 'style' => 'primary'],
['label' => 'Reject', 'url' => '/docs/1/reject', 'style' => 'danger'],
],
]);
'deduplication' => [
'enabled' => true,
'window' => 300, // seconds
],
'rate_limit' => [
'enabled' => true,
'max_per_minute' => 30, // per user
],
React in your app without coupling to the package:
use CaiqueBispo\NotificationBell\Events\NotificationCreated;
use CaiqueBispo\NotificationBell\Events\NotificationRead;
use CaiqueBispo\NotificationBell\Events\NotificationArchived;
use CaiqueBispo\NotificationBell\Events\NotificationDeleted;
Event::listen(NotificationCreated::class, function ($event) {
// e.g. mirror to e-mail or Slack
$event->notification;
});
'broadcasting' => [
'enabled' => true,
'channel' => 'notifications.{user_id}',
'event' => 'NotificationCreated',
'private' => true,
'fallback_to_polling' => true,
],
With Laravel Echo (Reverb/Pusher) configured in the host app, the bell listens over websockets. Register the channel authorization in your routes/channels.php:
Broadcast::channel('notifications.{userId}', fn ($user, $userId) => (int) $user->id === (int) $userId);
If Echo is absent, the component keeps working via polling.
For mobile apps and SPAs:
'api' => [
'enabled' => true,
'prefix' => 'api/notifications',
'middleware' => ['api', 'auth:sanctum'],
],
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/notifications |
Paginated list (?status=unread|read|archived|pinned, ?type=, ?category=, ?search=) |
| GET | /api/notifications/unread-count |
Unread counter |
| GET | /api/notifications/stats |
Totals by state and type |
| POST | /api/notifications/{id}/read |
Mark as read |
| POST | /api/notifications/read-all |
Mark all as read |
| POST | /api/notifications/{id}/pin |
Toggle pin |
| POST | /api/notifications/{id}/archive |
Archive |
| POST | /api/notifications/{id}/unarchive |
Unarchive |
| DELETE | /api/notifications/{id} |
Soft delete |
| POST | /api/notifications/{id}/restore |
Undo delete |
| GET | /api/notifications/preferences |
Get user preferences |
| PUT | /api/notifications/preferences |
Update user preferences |
All endpoints operate strictly on the authenticated user's own notifications.
A full-page notification center at /notifications/history with search, filters (status, type, category, period), pin/archive actions and incremental loading. Linked from the bell's "View all" footer. Disable with 'features' => ['history_page' => false].
Comprehensive management panel at /notifications (configurable): create/edit/delete, filters, send to one or all users, bulk actions, stats — fully redesigned with self-contained CSS.
'route' => [
'prefix' => 'admin/notifications',
'middleware' => ['web', 'auth', 'role:admin'],
'name' => 'admin.notifications.',
],
Ships with English (default) and Brazilian Portuguese. By default the component follows app()->getLocale(), falling back to English. To force a language regardless of the app locale:
// config/notifications.php
'locale' => 'pt_BR', // null = follow the app locale
Or per component instance:
<livewire:notification-bell locale="pt_BR" />
This applies to every text in the bell, history page and admin panel — including relative dates ("há 4 meses" instead of "4 months ago"). To customize the strings:
php artisan vendor:publish --tag="notification-bell-lang"
Generated with the Web Audio API (no audio files) — unique tones per type — or bring your own file:
'features' => [
'sound' => [
'enabled' => true,
'volume' => 0.5,
'file' => '/sounds/notify.mp3', // optional custom sound
],
],
Browsers may block audio until the user interacts with the page (standard autoplay policy).
# Cleanup (respects keep_pinned / keep_archived, purges soft-deleted trash)
php artisan notifications:cleanup
php artisan notifications:cleanup --days=60 --unread-days=120
php artisan notifications:cleanup --dry-run
# Bulk send
php artisan notifications:send-bulk "Title" "Message" --all-users
php artisan notifications:send-bulk "Maintenance" "Tonight 22h" --users=1,2,3 --type=warning
Schedule cleanup in routes/console.php (or Kernel.php on older versions):
Schedule::command('notifications:cleanup')->daily();
Notifications are processed through Laravel queues by default. Run a worker:
php artisan queue:work
Or bypass per call with 'queue' => false in the options array.
composer install
composer test
The suite uses Orchestra Testbench with in-memory SQLite — no app scaffolding required.
The package is designed to never take a production page down:
composer update and php artisan migrate, the bell detects the outdated schema and renders empty (logging a warning) instead of throwing a QueryException on every page. It recovers automatically on the first request after the migration runs.Every user-facing capability can be turned off individually:
'features' => [
'pin' => true,
'archive' => true,
'clear_all' => true,
'undo_delete' => ['enabled' => true, 'window' => 8000],
'grouping' => ['enabled' => true, 'min_size' => 3],
'history_page' => true,
// toasts and sound: see above
],
php artisan vendor:publish --tag="notification-bell-config" --force to see the new keys (locale, theme, preferences, categories, deduplication, rate_limit, api...). An old published config keeps working — missing keys fall back to the package defaults.category, group_key, dedup_key, image_url, pinned_at, archived_at, scheduled_at, expires_at, deleted_at) and the notification_preferences table — just run php artisan migrate.Notification::delete() is now a soft delete. Cleanup routines purge trashed rows.$options array.theme.item_view / theme.bell_icon.MIT License - see the LICENSE file for details.
How can I help you explore Laravel packages today?