Notification Badge

Notification Bell with animated notification badge and pop-up with various configurations.

Preview

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

Installation

1. Run the following command in your terminal:
npx shadcn@latest add https://refinery.abhii.me/r/notification-badge.json

API Reference

Component props
NameTypeDescription
notificationsNotificationItem[]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.
panelTitlestringTitle text displayed at the top of the notification panel.
markAllReadLabelstringLabel for the button that marks all notifications as read.
footerLabelstringLabel for the footer link/button that typically leads to a full notifications page.
emptyLabelstringText displayed in the panel when there are no notifications to show.
onNotificationClickDispatch<NotificationItem>Callback function fired when a notification item is clicked. Receives the clicked notification as an argument.
onDismissDispatch<NotificationItem>Callback function fired when a notification is dismissed.
onMarkAllRead() => voidCallback function fired when the 'Mark all as read' button is clicked.
onFooterClick() => voidCallback function fired when the footer link/button is clicked.
triggerFunctionComponent<NotificationTriggerProps>Custom element to use as the trigger for the notification panel. If not provided, a default bell icon will be used.
classNamestringAdditional CSS classes to apply to the trigger element for custom styling.
panelClassNamestringAdditional CSS classes to apply to the notification panel for custom styling.

Examples

Below are usage code examples of different variant usecases shown above in the preview section.

1

Default Bell -

Minimal variant using the default bell icon trigger and dot badge.

minimal.tsx
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,
        },
      ]}
    />
  );
}
2

Count Badge -

Variant showing a count of unread notifications instead of a simple dot.

count.tsx
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>
  );
}
3

Empty badge -

Notification badge with no notifications - dot hidden but panel still shows empty state when triggered.

empty-badge.tsx
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}
    />
  );
}
4

Fully Empty -

Notification badge in a fully empty state with no notifications and custom empty message in the panel.

fully-empty.tsx
import NotificationBadge from "@/components/notification-badge";

export default function NotificationBadgeExample4() {
  return (
    <NotificationBadge
      notifications={[]}
      emptyLabel="You're all caught up ✓"
      footerLabel={null}
      markAllReadLabel={null}
    />
  );
}
5

Custom Trigger -

Swap out the default bell icon for your own custom trigger element, such as a profile picture or a different icon.

custom-trigger.tsx
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>
      )}
    />
  );
}
6

With Icons -

Notification badge with lucide icons instead of emojis.

with-icons.tsx
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,
        },
      ]}
    />
  );
}
7

Externally Controlled state -

Manage the open/closed state externally, such as with polling for new notifications or a websocket connection.

controlled-external.tsx
"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 })))
      }
    />
  );
}

Usage Recommendations

Use the notification badge in your app's header or navigation area where users expect to see alerts.
Keep the notification messages concise for better readability in the pop-up panel.
Offers great flexibility with custom triggers and styling, so feel free to adapt it to fit your app's design and user experience needs.
Design Notes

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.