Notification Bell with animated notification badge and pop-up with various configurations.
This component is designed to be flexible and can be used for a variety of notification styles. Feel free to customize the content, styling, and behavior to fit your specific use case!
Browse the example below and also you can see code of each previews in the below 'Examples' section.
1. Minimal ~ dot only, no footer
npx shadcn@latest add https://refinery.abhii.me/r/notification-badge.json| Name | Type | Description |
|---|---|---|
| notifications | NotificationItem[] | List of notifications to display in the panel. |
| badgeVariant | "dot" | "count" | Style of the notification badge. 'dot' shows a simple dot, 'count' shows the number of notifications. |
| panelTitle | string | Title text displayed at the top of the notification panel. |
| markAllReadLabel | string | Label for the button that marks all notifications as read. |
| footerLabel | string | Label for the footer link/button that typically leads to a full notifications page. |
| emptyLabel | string | Text displayed in the panel when there are no notifications to show. |
| onNotificationClick | Dispatch<NotificationItem> | Callback function fired when a notification item is clicked. Receives the clicked notification as an argument. |
| onDismiss | Dispatch<NotificationItem> | Callback function fired when a notification is dismissed. |
| onMarkAllRead | () => void | Callback function fired when the 'Mark all as read' button is clicked. |
| onFooterClick | () => void | Callback function fired when the footer link/button is clicked. |
| trigger | FunctionComponent<NotificationTriggerProps> | Custom element to use as the trigger for the notification panel. If not provided, a default bell icon will be used. |
| className | string | Additional CSS classes to apply to the trigger element for custom styling. |
| panelClassName | string | Additional CSS classes to apply to the notification panel for custom styling. |
Below are usage code examples of different variant usecases shown above in the preview section.
Minimal variant using the default bell icon trigger and dot badge.
import NotificationBadge from "@/components/notification-badge";
export default function NotificationBadgeExample1() {
return (
<NotificationBadge
badgeVariant="dot"
footerLabel={null}
notifications={[
{
id: 1,
message: "Deployment succeeded",
timestamp: "just now",
unread: true,
},
{
id: 2,
message: "PR #42 was merged",
timestamp: "5 min ago",
unread: true,
},
]}
/>
);
}
Variant showing a count of unread notifications instead of a simple dot.
import NotificationBadge from "@/components/notification-badge";
import { useRouter } from "next/navigation";
export default function NotificationBadgeExample2() {
const router = useRouter();
const dismissNotification = (id: string | number) => {
console.log(`Dismiss notification with id: ${id}`);
};
const markAllRead = () => {
console.log("Mark all notifications as read");
};
return (
<div className="flex w-full flex-col items-center justify-start">
<NotificationBadge
badgeVariant="count"
notifications={[
{
id: 1,
message: "Sarah left a comment on your post",
timestamp: "2 min ago",
icon: "💬",
unread: true,
},
{
id: 2,
message: "Your invoice #1084 is ready",
timestamp: "1 hr ago",
icon: "🧾",
unread: true,
},
{
id: 3,
message: "Backup completed successfully",
timestamp: "3 hr ago",
icon: "✅",
unread: false,
},
]}
onNotificationClick={(item) => router.push(`/notifications/${item.id}`)}
onDismiss={(item) => dismissNotification(item.id)}
onMarkAllRead={() => markAllRead()}
onFooterClick={() => router.push("/notifications")}
/>
</div>
);
}
Notification badge with no notifications - dot hidden but panel still shows empty state when triggered.
import NotificationBadge from "@/components/notification-badge";
export default function NotificationBadgeExample3() {
return (
<NotificationBadge
notifications={[
{
id: 1,
message: "Welcome to Refinery!",
timestamp: "yesterday",
unread: false,
},
]}
emptyLabel="Nothing new right now"
markAllReadLabel={null}
/>
);
}
Notification badge in a fully empty state with no notifications and custom empty message in the panel.
import NotificationBadge from "@/components/notification-badge";
export default function NotificationBadgeExample4() {
return (
<NotificationBadge
notifications={[]}
emptyLabel="You're all caught up ✓"
footerLabel={null}
markAllReadLabel={null}
/>
);
}
Swap out the default bell icon for your own custom trigger element, such as a profile picture or a different icon.
import NotificationBadge from "@/components/notification-badge";
import { cn } from "@/lib/utils";
import { BellIcon } from "lucide-react";
export default function NotificationBadgeExample5() {
const notifications = [
{
id: 1,
message: "New comment on your post",
timestamp: "2 min ago",
unread: true,
},
{
id: 2,
message: "Your order has shipped",
timestamp: "1 hr ago",
unread: true,
},
{
id: 3,
message: "Password changed successfully",
timestamp: "3 hr ago",
unread: false,
},
];
return (
<NotificationBadge
badgeVariant="count"
notifications={notifications}
trigger={({ open, onClick, unreadCount }) => (
<button
onClick={onClick}
className={cn(
"relative flex items-center gap-2 rounded-lg px-3 py-1.5 text-sm font-medium",
"border border-neutral-200 dark:border-neutral-700",
"transition-colors hover:bg-neutral-100 dark:hover:bg-neutral-800",
open && "bg-neutral-100 dark:bg-neutral-800"
)}
>
<BellIcon className="size-4" />
Updates
{unreadCount > 0 && (
<span className="flex size-4 items-center justify-center rounded-full bg-red-500 text-[10px] font-semibold text-white">
{unreadCount}
</span>
)}
</button>
)}
/>
);
}
Notification badge with lucide icons instead of emojis.
import NotificationBadge from "@/components/notification-badge";
import {
AlertCircle,
GitPullRequest,
MessageSquare,
Rocket,
} from "lucide-react";
export default function NotificationBadgeExample6() {
return (
<NotificationBadge
panelTitle="Activity"
footerLabel="Open activity feed"
notifications={[
{
id: 1,
message: "PR #88 needs your review",
timestamp: "4 min ago",
icon: <GitPullRequest className="size-3.5 text-violet-500" />,
unread: true,
},
{
id: 2,
message: "Production deploy finished",
timestamp: "20 min ago",
icon: <Rocket className="size-3.5 text-emerald-500" />,
unread: true,
},
{
id: 3,
message: "Alex mentioned you in #general",
timestamp: "1 hr ago",
icon: <MessageSquare className="size-3.5 text-blue-500" />,
unread: false,
},
{
id: 4,
message: "High memory usage on worker-2",
timestamp: "2 hr ago",
icon: <AlertCircle className="size-3.5 text-amber-500" />,
unread: false,
},
]}
/>
);
}
Manage the open/closed state externally, such as with polling for new notifications or a websocket connection.
"use client";
import NotificationBadge, {
NotificationItem,
} from "@/components/notification-badge";
import { useEffect, useState } from "react";
export default function NotificationBadgeExample7() {
const [notifications, setNotifications] = useState<NotificationItem[]>([]);
const subscribeToNotifications = (
callback: (item: NotificationItem) => void
) => {
// Simulate incoming notifications every 5 seconds
const interval = setInterval(() => {
const newNotification = {
id: Date.now(),
message: `New notification at ${new Date().toLocaleTimeString()}`,
timestamp: "just now",
unread: true,
};
callback(newNotification);
}, 5000);
// Return unsubscribe function
return () => clearInterval(interval);
};
useEffect(() => {
const unsub = subscribeToNotifications((incoming) => {
setNotifications((prev) => [incoming, ...prev].slice(0, 10));
});
return unsub;
}, []);
return (
<NotificationBadge
notifications={notifications}
onDismiss={(item) =>
setNotifications((prev) => prev.filter((n) => n.id !== item.id))
}
onMarkAllRead={() =>
setNotifications((prev) => prev.map((n) => ({ ...n, unread: false })))
}
/>
);
}
Component is set up around a trigger element, with a bell icon as the default, that toggles a notification panel on click. You can show either a dot or an unread count with badgeVariant, pass your own trigger, and style the panel through props. The panel uses motion/react for smooth enter and exit states, and each notification item exposes click and dismiss handlers so you can wire it into real app behavior.