Notification Center
Slide-in notification panel with tabs, filtering, and action buttons.
Live Preview
Key Features
- Drawer panel sliding in from the right
- Tab filtering for All and Unread notifications
- Notification items with Avatar and timestamps
- Unread count Badge on trigger button
- Mark as read and archive actions with Tooltip
- Empty state when no notifications match filter
Code
Notification Center Recipe
<script lang="ts">
import { Drawer, Tab, TabItem, TabPanel, Badge, Avatar, Button, Tooltip } from '@urbicon-ui/blocks';
interface Notification {
id: string;
title: string;
description: string;
sender: string;
time: string;
read: boolean;
type: 'info' | 'success' | 'warning';
}
let drawerOpen = $state(false);
let activeTab = $state('all');
let notifications = $state<Notification[]>([
{
id: '1',
title: 'Commented on your design',
description: 'Left feedback on the Dashboard redesign file.',
sender: 'Sarah Chen',
time: '2 min ago',
read: false,
type: 'info'
},
{
id: '2',
title: 'Build #1234 succeeded',
description: 'Production deployment completed successfully.',
sender: 'CI Bot',
time: '15 min ago',
read: false,
type: 'success'
},
{
id: '3',
title: 'New team member invited',
description: 'Marcus Rivera invited to Engineering workspace.',
sender: 'Aisha Patel',
time: '1 hour ago',
read: false,
type: 'info'
},
{
id: '4',
title: 'Storage quota warning',
description: 'Workspace has used 85% of allocated storage.',
sender: 'System',
time: '2 hours ago',
read: true,
type: 'warning'
}
]);
let unreadCount = $derived(notifications.filter((n) => !n.read).length);
let unreadNotifications = $derived(notifications.filter((n) => !n.read));
function markAsRead(id: string) {
notifications = notifications.map((n) =>
n.id === id ? { ...n, read: true } : n
);
}
function archiveNotification(id: string) {
notifications = notifications.filter((n) => n.id !== id);
}
function markAllAsRead() {
notifications = notifications.map((n) => ({ ...n, read: true }));
}
</script>
<!-- Trigger Button -->
<Button variant="outlined" intent="neutral" onclick={() => (drawerOpen = true)}>
<span class="relative inline-flex items-center gap-2">
<BellIcon size={20} />
Notifications
{#if unreadCount > 0}
<Badge variant="soft" intent="danger" size="sm">{unreadCount}</Badge>
{/if}
</span>
</Button>
<!-- Notification Drawer -->
<Drawer bind:open={drawerOpen} title="Notifications" placement="right" size="md" onClose={() => (drawerOpen = false)}>
<Tab bind:value={activeTab} variant="line" size="sm">
{#snippet tabs()}
<TabItem value="all">All</TabItem>
<TabItem value="unread">
Unread
{#if unreadCount > 0}
<Badge variant="soft" intent="danger" size="sm" class="ml-1.5">{unreadCount}</Badge>
{/if}
</TabItem>
{/snippet}
{#snippet panels()}
<TabPanel value="all">
<div class="divide-y divide-border-subtle">
{#each notifications as notification (notification.id)}
<!-- Notification item -->
<div class={['flex gap-3 px-1 py-3', notification.read && 'opacity-60']}>
<Avatar name={notification.sender} size="sm" />
<div class="min-w-0 flex-1">
<p class="text-sm font-medium text-text-primary">{notification.title}</p>
<p class="mt-0.5 text-xs text-text-secondary">{notification.description}</p>
<p class="mt-1 text-xs text-text-quaternary">{notification.sender} · {notification.time}</p>
</div>
<div class="flex shrink-0 items-start gap-1">
{#if !notification.read}
<Tooltip label="Mark as read">
<button class="rounded p-1 text-text-tertiary transition-colors hover:bg-surface-hover hover:text-primary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50" aria-label="Mark as read" onclick={() => markAsRead(notification.id)}>
<CheckIcon size={16} />
</button>
</Tooltip>
{/if}
<Tooltip label="Archive">
<button class="rounded p-1 text-text-tertiary transition-colors hover:bg-surface-hover hover:text-primary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50" aria-label="Archive" onclick={() => archiveNotification(notification.id)}>
<ArchiveIcon size={16} />
</button>
</Tooltip>
</div>
</div>
{/each}
</div>
</TabPanel>
<TabPanel value="unread">
<!-- Same structure, filtered to unread only -->
</TabPanel>
{/snippet}
</Tab>
{#snippet footer()}
<Button size="sm" variant="ghost" intent="neutral" onclick={markAllAsRead} disabled={unreadCount === 0}>
Mark all as read
</Button>
{/snippet}
</Drawer>