Menu

Change language

GitHub repo

Notification Center

blockcommunication

Full-page notification center: unread badge, mark-all-read, All/Unread/Mentions tabs with functional filtering, day-grouped rows (Today / Earlier) pairing category icon tiles with titles, bodies, relative timestamps, unread dots and hover actions (mark read, dismiss). Per-tab empty states included; swap `notifications` for your source.

Installation

$npx shadcn-svelte@latest add https://uipkge.dev/r/svelte/notification-center.json
Manifest:https://uipkge.dev/r/svelte/notification-center.jsonInstalls to:src/lib/components/blocks/notification-center/

Variants

Loading interactive previews…

Files installed (1)

  • src/lib/components/blocks/notification-center/NotificationCenter.svelte7.6 kB
    <script lang="ts" module>
      import type { Component } from 'svelte'
    
      export type NotificationCategory = 'mention' | 'comment' | 'system' | 'security'
    
      export interface Notification {
        id: string
        category: NotificationCategory
        title: string
        body: string
        date: Date
        read: boolean
      }
    
      export interface NotificationCenterProps {
        notifications?: Notification[]
        class?: string
      }
    </script>
    
    <script lang="ts">
      import { BellOff, Check, CheckCheck, MessageSquare, ShieldAlert, UserPlus, X } from '@lucide/svelte'
      import { cn } from '$lib/utils'
      import { Badge } from '$lib/components/ui/badge'
      import { Button } from '$lib/components/ui/button'
      import { RelativeTime } from '$lib/components/ui/relative-time'
      import { SectionCard } from '$lib/components/ui/section-card'
      import { Tabs, TabsList, TabsTrigger } from '$lib/components/ui/tabs'
    
      let { notifications: notificationsProp, class: className }: NotificationCenterProps = $props()
    
      const categoryConfig: Record<NotificationCategory, { icon: Component; tileClass: string }> = {
        mention: { icon: UserPlus, tileClass: 'bg-primary/10 text-primary' },
        comment: { icon: MessageSquare, tileClass: 'bg-chart-3/10 text-chart-3' },
        system: { icon: BellOff, tileClass: 'bg-muted text-muted-foreground' },
        security: { icon: ShieldAlert, tileClass: 'bg-warning/10 text-warning' },
      }
    
      const now = new Date()
      const minutesAgo = (m: number) => new Date(now.getTime() - m * 60_000)
      const hoursAgo = (h: number) => new Date(now.getTime() - h * 3_600_000)
      const daysAgo = (d: number) => new Date(now.getTime() - d * 86_400_000)
    
      const stubNotifications: Notification[] = [
        {
          id: 'n1',
          category: 'mention',
          title: 'Priya mentioned you',
          body: '@amara can you review the onboarding flow?',
          date: minutesAgo(6),
          read: false,
        },
        {
          id: 'n2',
          category: 'security',
          title: 'New sign-in from Berlin',
          body: 'Chrome on macOS · 84.190.201.3',
          date: minutesAgo(52),
          read: false,
        },
        {
          id: 'n3',
          category: 'comment',
          title: 'Jonas commented',
          body: 'Shipped the fix — closing the ticket.',
          date: hoursAgo(2),
          read: false,
        },
        {
          id: 'n4',
          category: 'system',
          title: 'Weekly digest ready',
          body: 'Your workspace summary for last week.',
          date: hoursAgo(20),
          read: true,
        },
        {
          id: 'n5',
          category: 'mention',
          title: 'Marcus mentioned you',
          body: 'Design review moved to 15:00, @amara.',
          date: daysAgo(1),
          read: false,
        },
        {
          id: 'n6',
          category: 'comment',
          title: 'Amara commented',
          body: 'Copy updated per legal review.',
          date: daysAgo(1),
          read: true,
        },
        {
          id: 'n7',
          category: 'security',
          title: 'API key rotated',
          body: 'uipkge_live_…4f2a was rotated automatically.',
          date: daysAgo(2),
          read: true,
        },
      ]
    
      let items = $state<Notification[]>(stubNotifications)
      $effect(() => {
        items = notificationsProp ?? stubNotifications
      })
      let filter = $state<'all' | 'unread' | 'mentions'>('all')
    
      const unreadCount = $derived(items.filter((n) => !n.read).length)
    
      const filtered = $derived(
        items.filter((n) => {
          if (filter === 'unread') return !n.read
          if (filter === 'mentions') return n.category === 'mention'
          return true
        }),
      )
    
      const groups = $derived.by(() => {
        const today: Notification[] = []
        const earlier: Notification[] = []
        const startOfToday = new Date(now)
        startOfToday.setHours(0, 0, 0, 0)
        for (const notification of filtered) {
          ;(notification.date >= startOfToday ? today : earlier).push(notification)
        }
        return [
          { label: 'Today', items: today },
          { label: 'Earlier', items: earlier },
        ].filter((group) => group.items.length > 0)
      })
    
      function markAllRead() {
        for (const notification of items) notification.read = true
      }
    </script>
    
    <SectionCard
      data-slot="notification-center"
      title="Notifications"
      description="Everything that needs your attention."
      class={className}
    >
      {#snippet headerAction()}
        <div class="flex flex-wrap items-center gap-2">
          {#if unreadCount > 0}
            <Badge variant="default">{unreadCount} new</Badge>
          {/if}
          <Button variant="ghost" size="sm" disabled={unreadCount === 0} onclick={markAllRead}>
            <CheckCheck aria-hidden="true" />
            Mark all read
          </Button>
        </div>
      {/snippet}
    
      <Tabs
        value={filter}
        onValueChange={(v) => {
          if (v === 'all' || v === 'unread' || v === 'mentions') filter = v
        }}
        class="mb-2"
      >
        <TabsList>
          <TabsTrigger value="all">All</TabsTrigger>
          <TabsTrigger value="unread">Unread</TabsTrigger>
          <TabsTrigger value="mentions">Mentions</TabsTrigger>
        </TabsList>
      </Tabs>
    
      {#if groups.length === 0}
        <div class="text-muted-foreground flex flex-col items-center gap-2 py-12 text-sm">
          <BellOff class="size-6 opacity-50" aria-hidden="true" />
          {#if filter === 'mentions'}
            No mentions — you're all caught up.
          {:else if filter === 'unread'}
            Nothing unread. Nice work.
          {:else}
            Your inbox is empty.
          {/if}
        </div>
      {/if}
    
      {#each groups as group (group.label)}
        <div class="mb-2">
          <p class="text-muted-foreground py-1.5 text-xs font-medium tracking-widest uppercase">{group.label}</p>
          <ul class="divide-y">
            {#each group.items as notification (notification.id)}
              {@const Icon = categoryConfig[notification.category].icon}
              <li
                class="group hover:bg-accent/40 -mx-2 flex cursor-pointer items-start gap-3 rounded-md px-2 py-3 transition-colors"
                onclick={() => (notification.read = true)}
              >
                <span
                  class={cn(
                    'mt-0.5 flex size-8 shrink-0 items-center justify-center rounded-md',
                    categoryConfig[notification.category].tileClass,
                  )}
                  aria-hidden="true"
                >
                  <Icon class="size-4" />
                </span>
                <div class="min-w-0 flex-1">
                  <div class="flex flex-wrap items-center gap-2">
                    <p class="truncate text-sm {notification.read ? 'font-normal' : 'font-medium'}">
                      {notification.title}
                    </p>
                    {#if !notification.read}
                      <span class="bg-primary size-1.5 shrink-0 rounded-full" aria-label="Unread"></span>
                    {/if}
                  </div>
                  <p class="text-muted-foreground truncate text-xs">{notification.body}</p>
                </div>
                <div class="flex shrink-0 items-center gap-1">
                  <RelativeTime date={notification.date} class="text-muted-foreground text-xs" />
                  <span class="flex opacity-0 transition-opacity group-hover:opacity-100">
                    {#if !notification.read}
                      <Button
                        variant="ghost"
                        size="icon-sm"
                        aria-label="Mark as read"
                        onclick={(e) => {
                          e.stopPropagation()
                          notification.read = true
                        }}
                      >
                        <Check aria-hidden="true" />
                      </Button>
                    {/if}
                    <Button
                      variant="ghost"
                      size="icon-sm"
                      aria-label="Dismiss"
                      onclick={(e) => e.stopPropagation()}
                    >
                      <X aria-hidden="true" />
                    </Button>
                  </span>
                </div>
              </li>
            {/each}
          </ul>
        </div>
      {/each}
    </SectionCard>
    

Raw manifest:https://uipkge.dev/r/svelte/notification-center.json