Blox/
Notification Center
A comprehensive notification dropdown with filtering, read/unread states, different notification types, and action buttons. Essential for any application with user notifications.
Bell BadgeFilter TabsRead/UnreadType Icons
Installation
Terminal
TSXReactTailwind
npx @uiblox/cli add notification-centerPreview
Notifications
2SA
New message from Sarah
Hey! Checking in on the project...
2 minDeployment successful
App deployed to production.
1 hrStorage warning
80% of storage used.
3 hrPayment failed
Last payment not processed.
1 dayUse Cases
SaaS Applications
Keep users informed about system updates, billing alerts, and feature announcements.
Social Platforms
Display messages, mentions, likes, and follower activity in one unified feed.
E-commerce
Notify users about order updates, shipping status, and promotional offers.
Team Collaboration
Alert team members about task assignments, comments, and project updates.
Source Code
notification-center.tsx
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236"use client"; import { useState } from "react"; import { cn } from "@/lib/utils"; interface Notification { id: string; type: "info" | "success" | "warning" | "error" | "message"; title: string; description: string; time: string; read: boolean; avatar?: string; action?: { label: string; href: string }; } const initialNotifications: Notification[] = [ { id: "1", type: "message", title: "New message from Sarah", description: "Hey! Just wanted to check in on the project status...", time: "2 min ago", read: false, avatar: "SA", }, { id: "2", type: "success", title: "Deployment successful", description: "Your app has been deployed to production.", time: "1 hour ago", read: false, action: { label: "View", href: "#" }, }, { id: "3", type: "warning", title: "Storage limit warning", description: "You've used 80% of your storage quota.", time: "3 hours ago", read: true, action: { label: "Upgrade", href: "#" }, }, { id: "4", type: "info", title: "New feature available", description: "Check out our new analytics dashboard.", time: "1 day ago", read: true, }, { id: "5", type: "error", title: "Payment failed", description: "Your last payment could not be processed.", time: "2 days ago", read: true, action: { label: "Retry", href: "#" }, }, ]; export function NotificationCenter() { const [isOpen, setIsOpen] = useState(true); const [notifications, setNotifications] = useState(initialNotifications); const [filter, setFilter] = useState<"all" | "unread">("all"); const unreadCount = notifications.filter((n) => !n.read).length; const filteredNotifications = filter === "unread" ? notifications.filter((n) => !n.read) : notifications; const markAsRead = (id: string) => { setNotifications((prev) => prev.map((n) => (n.id === id ? { ...n, read: true } : n)) ); }; const markAllAsRead = () => { setNotifications((prev) => prev.map((n) => ({ ...n, read: true }))); }; const deleteNotification = (id: string) => { setNotifications((prev) => prev.filter((n) => n.id !== id)); }; const getTypeStyles = (type: Notification["type"]) => { switch (type) { case "success": return "bg-emerald-100 text-emerald-600 dark:bg-emerald-900/30 dark:text-emerald-400"; case "warning": return "bg-amber-100 text-amber-600 dark:bg-amber-900/30 dark:text-amber-400"; case "error": return "bg-red-100 text-red-600 dark:bg-red-900/30 dark:text-red-400"; case "message": return "bg-purple-100 text-purple-600 dark:bg-purple-900/30 dark:text-purple-400"; default: return "bg-blue-100 text-blue-600 dark:bg-blue-900/30 dark:text-blue-400"; } }; return ( <div className="relative"> {/* Bell Button */} <button onClick={() => setIsOpen(!isOpen)} className="relative p-2 rounded-lg hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors" > <BellIcon /> {unreadCount > 0 && ( <span className="absolute top-1 right-1 w-4 h-4 bg-red-500 text-white text-xs font-bold rounded-full flex items-center justify-center"> {unreadCount} </span> )} </button> {/* Dropdown Panel */} {isOpen && ( <div className="absolute right-0 mt-2 w-96 bg-white dark:bg-slate-900 rounded-xl border border-slate-200 dark:border-slate-800 shadow-xl overflow-hidden"> {/* Header */} <div className="flex items-center justify-between px-4 py-3 border-b border-slate-200 dark:border-slate-800"> <h3 className="font-semibold text-slate-900 dark:text-white">Notifications</h3> <div className="flex items-center gap-2"> <button onClick={markAllAsRead} className="text-xs text-purple-600 hover:text-purple-700 font-medium" > Mark all read </button> </div> </div> {/* Filter Tabs */} <div className="flex border-b border-slate-200 dark:border-slate-800"> <button onClick={() => setFilter("all")} className={cn( "flex-1 px-4 py-2 text-sm font-medium transition-colors", filter === "all" ? "text-purple-600 border-b-2 border-purple-600" : "text-slate-500 hover:text-slate-700 dark:hover:text-slate-300" )} > All </button> <button onClick={() => setFilter("unread")} className={cn( "flex-1 px-4 py-2 text-sm font-medium transition-colors", filter === "unread" ? "text-purple-600 border-b-2 border-purple-600" : "text-slate-500 hover:text-slate-700 dark:hover:text-slate-300" )} > Unread({unreadCount}) </button> </div> {/* Notification List */} <div className="max-h-96 overflow-y-auto"> {filteredNotifications.length === 0 ? ( <div className="p-8 text-center"> <p className="text-slate-500">No notifications</p> </div> ) : ( filteredNotifications.map((notification) => ( <div key={notification.id} onClick={() => markAsRead(notification.id)} className={cn( "flex gap-3 px-4 py-3 hover:bg-slate-50 dark:hover:bg-slate-800/50 cursor-pointer transition-colors border-b border-slate-100 dark:border-slate-800 last:border-0", !notification.read && "bg-purple-50/50 dark:bg-purple-900/10" )} > {/* Icon/Avatar */} {notification.avatar ? ( <div className="w-10 h-10 rounded-full bg-purple-600 flex items-center justify-center text-white font-semibold text-sm"> {notification.avatar} </div> ) : ( <div className={cn("w-10 h-10 rounded-full flex items-center justify-center", getTypeStyles(notification.type))}> <NotificationIcon type={notification.type} /> </div> )} {/* Content */} <div className="flex-1 min-w-0"> <div className="flex items-start justify-between gap-2"> <p className={cn("text-sm", !notification.read ? "font-semibold text-slate-900 dark:text-white" : "text-slate-700 dark:text-slate-300")}> {notification.title} </p> {!notification.read && ( <span className="w-2 h-2 bg-purple-600 rounded-full flex-shrink-0 mt-1.5" /> )} </div> <p className="text-sm text-slate-500 truncate">{notification.description}</p> <div className="flex items-center gap-3 mt-1"> <span className="text-xs text-slate-400">{notification.time}</span> {notification.action && ( <a href={notification.action.href} className="text-xs text-purple-600 hover:text-purple-700 font-medium"> {notification.action.label} </a> )} </div> </div> {/* Delete */} <button onClick={(e) => { e.stopPropagation(); deleteNotification(notification.id); }} className="p-1 text-slate-400 hover:text-red-500 rounded opacity-0 group-hover:opacity-100 hover:opacity-100 transition-opacity" > <XIcon /> </button> </div> )) )} </div> {/* Footer */} <div className="px-4 py-3 border-t border-slate-200 dark:border-slate-800"> <a href="#" className="block text-center text-sm text-purple-600 hover:text-purple-700 font-medium"> View all notifications </a> </div> </div> )} </div> ); } function BellIcon() { return <svg className="w-6 h-6 text-slate-600 dark:text-slate-400" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" /></svg>; } function XIcon() { return <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" /></svg>; } function NotificationIcon({ type }: { type: string }) { switch (type) { case "success": return <svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" /></svg>; case "warning": return <svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" /></svg>; case "error": return <svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" /></svg>; default: return <svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>; } }