Skip to main content
Urbicon UI
Back to Recipes

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} &middot; {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>