Overlays
Notification Center code
Copy the implementation or inspect every file that belongs to this component unit.
resources/views/components/ui/notification-center.blade.php
@props([
'notifications' => [],
'label' => 'Notifications',
'align' => 'right',
'emptyText' => "You're all caught up.",
'viewAllHref' => null,
'ghost' => false,
])
@php
/*
| Notification center — a bell trigger with a dropdown feed of recent
| notifications, unread badge and a "Mark all read" action. Pass rows as:
| ['title' => '...', 'description' => '...', 'time' => '2m', 'icon' => 'git-branch',
| 'unread' => true, 'href' => '/...', 'variant' => 'brand']
|
| Usage:
| <x-ui.notification-center :notifications="$rows" view-all-href="/notifications" />
| <x-ui.notification-center ghost />
|
| ghost — use a borderless icon-button trigger so the bell sits in a toolbar
| next to theme and account controls. An empty feed still opens the
| dropdown and shows emptyText.
*/
$items = collect($notifications)->map(function (array $n, int $i): array {
return [
'id' => $n['id'] ?? 'n'.$i,
'title' => $n['title'] ?? '',
'description' => $n['description'] ?? null,
'time' => $n['time'] ?? null,
'icon' => $n['icon'] ?? 'bell',
'unread' => (bool) ($n['unread'] ?? false),
'href' => $n['href'] ?? null,
'variant' => $n['variant'] ?? 'brand',
];
})->values()->all();
$menuAlign = $align === 'left' ? 'start-0' : 'end-0';
$variantDot = [
'brand' => 'bg-brand-500',
'green' => 'bg-green-500',
'amber' => 'bg-amber-500',
'red' => 'bg-red-500',
'blue' => 'bg-accent-500',
'gray' => 'bg-gray-400',
];
$variantIcon = [
'brand' => 'text-brand-600 dark:text-brand-400 bg-brand-50 dark:bg-brand-950/60',
'green' => 'text-green-600 dark:text-green-400 bg-green-50 dark:bg-green-950/60',
'amber' => 'text-amber-600 dark:text-amber-400 bg-amber-50 dark:bg-amber-950/60',
'red' => 'text-red-600 dark:text-red-400 bg-red-50 dark:bg-red-950/60',
'blue' => 'text-accent-600 dark:text-accent-400 bg-accent-50 dark:bg-accent-950/60',
'gray' => 'text-gray-600 dark:text-gray-300 bg-gray-100 dark:bg-gray-800',
];
@endphp
<div
x-data="{
open: false,
items: @js($items),
get unread() { return this.items.filter((n) => n.unread).length; },
markAllRead() { this.items.forEach((n) => (n.unread = false)); },
}"
{{ $attributes->merge(['class' => 'relative inline-block']) }}
>
<button
type="button"
@click="open = ! open"
:aria-expanded="open.toString()"
aria-haspopup="true"
data-notification-trigger
@class([
'relative inline-flex cursor-pointer items-center justify-center transition focus:outline-none',
'h-10 w-10 rounded-xl border border-gray-200 bg-white text-gray-600 hover:bg-gray-50 hover:text-gray-900 focus:ring-2 focus:ring-brand-500/30 dark:border-gray-800 dark:bg-gray-900 dark:text-gray-300 dark:hover:bg-gray-800 dark:hover:text-white' => ! $ghost,
'h-9 w-9 rounded-lg text-gray-500 hover:bg-gray-100 hover:text-gray-700 focus:ring-2 focus:ring-gray-400 focus:ring-offset-2 dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-gray-200 dark:focus:ring-offset-gray-900' => $ghost,
])
aria-label="{{ $label }}"
>
<x-ui.icon name="bell" weight="regular" size="lg" />
<span
x-show="unread > 0"
x-cloak
class="absolute -end-1 -top-1 inline-flex h-5 min-w-5 items-center justify-center rounded-full bg-red-500 px-1 text-[0.6875rem] font-bold text-white ring-2 ring-white dark:ring-gray-900"
x-text="unread > 9 ? '9+' : unread"
></span>
</button>
<div
x-show="open"
x-cloak
x-transition.origin.top
@click.outside="open = false"
@keydown.escape.window="open = false"
class="absolute {{ $menuAlign }} z-40 mt-2 w-80 origin-top overflow-hidden rounded-2xl border border-gray-200 bg-white shadow-xl shadow-gray-900/10 dark:border-gray-800 dark:bg-gray-900 sm:w-96"
role="dialog"
aria-label="{{ $label }}"
>
<div class="flex items-center justify-between gap-3 border-b border-gray-100 px-4 py-3 dark:border-gray-800">
<p class="text-sm font-semibold text-gray-900 dark:text-white">
{{ $label }}
<span x-show="unread > 0" x-cloak class="ms-1 text-gray-400" x-text="'(' + unread + ')'"></span>
</p>
<button
type="button"
@click="markAllRead()"
x-show="unread > 0"
x-cloak
class="text-xs font-semibold text-brand-600 transition hover:text-brand-500 dark:text-brand-400"
>Mark all read</button>
</div>
<div class="max-h-96 divide-y divide-gray-100 overflow-y-auto dark:divide-gray-800">
<template x-for="item in items" :key="item.id">
<a
:href="item.href || '#'"
class="flex items-start gap-3 px-4 py-3 transition hover:bg-gray-50 dark:hover:bg-gray-800/60"
:class="item.unread ? 'bg-brand-50/40 dark:bg-brand-950/20' : ''"
@click="item.unread = false"
>
<span
class="mt-0.5 inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-lg"
:class="{
'text-brand-600 dark:text-brand-400 bg-brand-50 dark:bg-brand-950/60': item.variant === 'brand',
'text-green-600 dark:text-green-400 bg-green-50 dark:bg-green-950/60': item.variant === 'green',
'text-amber-600 dark:text-amber-400 bg-amber-50 dark:bg-amber-950/60': item.variant === 'amber',
'text-red-600 dark:text-red-400 bg-red-50 dark:bg-red-950/60': item.variant === 'red',
'text-accent-600 dark:text-accent-400 bg-accent-50 dark:bg-accent-950/60': item.variant === 'blue',
'text-gray-600 dark:text-gray-300 bg-gray-100 dark:bg-gray-800': item.variant === 'gray',
}"
>
<i :class="'ph-fill ph-' + item.icon + ' text-lg leading-none'" aria-hidden="true"></i>
</span>
<span class="min-w-0 flex-1">
<span class="flex items-center gap-2">
<span class="truncate text-sm font-semibold text-gray-900 dark:text-white" x-text="item.title"></span>
<span x-show="item.unread" x-cloak class="h-2 w-2 shrink-0 rounded-full bg-brand-500"></span>
</span>
<span x-show="item.description" x-cloak class="mt-0.5 block text-xs leading-5 text-gray-500 dark:text-gray-400" x-text="item.description"></span>
<span x-show="item.time" x-cloak class="mt-1 block text-[0.6875rem] font-medium uppercase tracking-wide text-gray-400 dark:text-gray-500" x-text="item.time"></span>
</span>
</a>
</template>
<div x-show="items.length === 0" x-cloak data-notification-empty class="px-4 py-10 text-center">
<x-ui.icon name="check-circle" weight="fill" size="xl" class="text-gray-300 dark:text-gray-600" />
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">{{ $emptyText }}</p>
</div>
</div>
@if ($viewAllHref)
<a href="{{ $viewAllHref }}" class="block border-t border-gray-100 px-4 py-3 text-center text-sm font-semibold text-brand-600 transition hover:bg-gray-50 dark:border-gray-800 dark:text-brand-400 dark:hover:bg-gray-800/60">
View all notifications
</a>
@endif
</div>
</div>