Notification Center
blockcommunicationFull-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
$pnpm dlx shadcn-svelte@latest add https://uipkge.dev/r/svelte/notification-center.json$npx shadcn-svelte@latest add https://uipkge.dev/r/svelte/notification-center.json$yarn dlx shadcn-svelte@latest add https://uipkge.dev/r/svelte/notification-center.json$bunx shadcn-svelte@latest add https://uipkge.dev/r/svelte/notification-center.jsonManifest:
https://uipkge.dev/r/svelte/notification-center.jsonInstalls to:src/lib/components/blocks/notification-center/Variants
Loading interactive previews…
Loading interactive previews…
npm dependencies
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