Build a Notification Center with Next.js.

Today, we’ll create a Slack-style notification inbox using HeroUI Tabs, Badges, and Toasts.
This project is powered by Next.js and @heroui/react with TypeScript.

You’ll learn to build a Notification Center that feels modern, fast, and interactive. Let’s dive in!

Why Build a Notification Center?

Notifications help users stay updated. A good notification inbox organizes alerts, shows counts, and allows quick actions.

Instead of sending all messages at once, we’ll use Tabs for grouping, Badges for counts, and Toasts for real-time feedback.

This pattern mirrors Slack, Gmail, or Discord. Users see a clean interface where new updates never get lost.

By the end, you’ll have a reusable component for any Next.js app.

Setting Up Next.js with HeroUI

Creating the Notification Tabs

Let’s build the inbox UI. Tabs will separate All, Mentions, and System notifications.

Inside app/notif/page.tsx, add:

"use client";

import { Tabs, Tab, Badge } from "@heroui/react";
import { useState } from "react";
import {
  BellAlertIcon,
  UserGroupIcon,
  Cog6ToothIcon,
} from "@heroicons/react/24/outline";

export default function NotificationCenter() {
  const [unread, setUnread] = useState({ all: 5, mentions: 2, system: 1 });

  return (
    <div className="max-w-md mx-auto mt-10 p-4 border rounded-lg shadow">
      <h2 className="text-xl font-bold mb-4">Notifications</h2>
      <Tabs aria-label="Notifications" color="primary" fullWidth>
        <Tab
          key="all"
          title={
            <Badge color="danger" content={unread.all}>
              <div className="flex items-center space-x-2">
                <span className="text-base font-medium">All</span>
                <BellAlertIcon className="h-6 w-6" />
              </div>
            </Badge>
          }
        >
          <p>All notifications go here</p>
        </Tab>
        <Tab
          key="mentions"
          title={
            <Badge color="danger" content={unread.mentions}>
              <div className="flex items-center space-x-2">
                <span className="text-base font-medium">Mentions</span>
                <UserGroupIcon className="h-6 w-6" />
              </div>
            </Badge>
          }
        >
          <p>Mentions notifications go here</p>
        </Tab>
        <Tab
          key="system"
          title={
            <Badge color="danger" content={unread.system}>
              <div className="flex items-center space-x-2">
                <span className="text-base font-medium">System</span>
                <Cog6ToothIcon className="h-6 w-6" />
              </div>
            </Badge>
          }
        >
          <p>System notifications go here</p>
        </Tab>
      </Tabs>
    </div>
  );
}

Here’s what happens:

  • Tabs control sections.
  • Badge shows unread counts.
  • State tracks numbers dynamically.

Already, you’ve got a neat inbox with categories!

Notification

Adding Toast Notifications

Next, let’s add Toasts. Toasts are short pop-up alerts that appear at the bottom of the screen.

Install the HeroUI Toast hook:

// app/providers.tsx
"use client";

import type { ThemeProviderProps } from "next-themes";

import * as React from "react";
import { HeroUIProvider } from "@heroui/system";
import { useRouter } from "next/navigation";
import { ThemeProvider as NextThemesProvider } from "next-themes";
import { ToastProvider } from "@heroui/toast";

export interface ProvidersProps {
  children: React.ReactNode;
  themeProps?: ThemeProviderProps;
}

declare module "@react-types/shared" {
  interface RouterConfig {
    routerOptions: NonNullable<
      Parameters<ReturnType<typeof useRouter>["push"]>[1]
    >;
  }
}

export function Providers({ children, themeProps }: ProvidersProps) {
  const router = useRouter();

  return (
    <HeroUIProvider navigate={router.push}>
      <ToastProvider />
      <NextThemesProvider {...themeProps}>{children}</NextThemesProvider>
    </HeroUIProvider>
  );
}

Add Toast:

"use client";

import { Tabs, Tab, Badge, Button } from "@heroui/react";
import { useState } from "react";
import { addToast } from "@heroui/toast";
import {
  BellAlertIcon,
  UserGroupIcon,
  Cog6ToothIcon,
} from "@heroicons/react/24/outline";

export default function NotificationCenter() {
  const [unread] = useState({ all: 5, mentions: 2, system: 1 });

  const triggerToast = () => {
    addToast({
      title: "You’ve got a new notification!",
      timeout: 3000,
    });
  };

  return (
    <div className="max-w-md mx-auto mt-10 p-4 border rounded-lg shadow">
      <h2 className="text-xl font-bold mb-4">Notifications</h2>
      <Tabs aria-label="Notifications" color="primary" fullWidth={true}>
        <Tab
          key="all"
          title={
            <Badge color="danger" content={unread.all}>
              <div className="flex items-center space-x-2">
                <span className="text-base font-medium">All</span>
                <BellAlertIcon className="h-6 w-6" />
              </div>
            </Badge>
          }
        >
          <p>All notifications go here</p>
        </Tab>
        <Tab
          key="mentions"
          title={
            <Badge color="danger" content={unread.mentions}>
              <div className="flex items-center space-x-2">
                <span className="text-base font-medium">Mentions</span>
                <UserGroupIcon className="h-6 w-6" />
              </div>
            </Badge>
          }
        >
          <p>Mentions notifications go here</p>
        </Tab>
        <Tab
          key="system"
          title={
            <Badge color="danger" content={unread.system}>
              <div className="flex items-center space-x-2">
                <span className="text-base font-medium">System</span>
                <Cog6ToothIcon className="h-6 w-6" />
              </div>
            </Badge>
          }
        >
          <p>System notifications go here</p>
        </Tab>
      </Tabs>
      <div className="p-4">
        <Button color="primary" onPress={triggerToast}>
          Show Toast
        </Button>
      </div>
    </div>
  );
}

Now, when you click the button, a toast appears.

Toasts are ideal for delivering real-time updates, such as new messages, system alerts, or confirmations.

Combine this with sockets or APIs, and your users stay informed instantly.

Show toast button
Add toast

Connecting Tabs and Toasts

Let’s connect Tabs and Toasts so they update together. Imagine receiving a new mention.

We’ll increment the Mentions badge and show a toast.

const [unread, setUnread] = useState({ all: 5, mentions: 2, system: 1 });
  
const addMention = () => {
    setUnread((prev) => ({
      ...prev,
      all: prev.all + 1,
      mentions: prev.mentions + 1,
    }));
    addToast({
      title: "Someone mentioned you!",
    });
  };      


<div className="p-4 flex space-x-4">
  <Button color="primary" onPress={triggerToast}>
    Show Toast
  </Button>
  <Button color="primary" onPress={addMention}>
    Simulate Mention
  </Button>
</div>

Now pressing “Simulate Mention” updates both the Tab count and shows a Toast. That’s how you sync real-time alerts with the inbox view!

Simulate Mention button
Add Mention

Enhancing the Inbox Experience

At this point, the Notification Center is functioning properly. But let’s enhance usability:

  1. Scrollable Lists — Show long notification histories with smooth scroll.
  2. Clickable Items — Each notification can be linked to a specific page.
  3. Mark as Read — Clicking a notification should decrease the badge count.

Example of a list inside a Tab:

const notifications = [
  { id: 1, text: "Alice mentioned you", type: "mention" },
  { id: 2, text: "Server restarted", type: "system" },
];

<Tab key="all" title={<Badge content={unread.all}>All</Badge>}>
  <ul>
    {notifications.map((n) => (
      <li key={n.id} className="p-2 border-b hover:bg-gray-50 cursor-pointer">
        {n.text}
      </li>
    ))}
  </ul>
</Tab>

This structure mirrors Slack or Gmail, where each notification is interactive.

Add simple onClick handlers to mark items as read and update the state.

Lists

Example of a Mark as Read:

import React from "react";
import { Key } from "@react-types/shared";

const [selected, setSelected] = React.useState("all");

const handleSelectionChange = (key: Key) => {
  if (typeof key === "string") {
    setSelected(key);
  }

  if (key === "all") {
    setUnread((prev) => ({
      ...prev,
      all: 0,
    }));
  } else if (key === "mentions") {
    setUnread((prev) => ({
      ...prev,
      mentions: 0,
    }));
  } else if (key === "system") {
    setUnread((prev) => ({
      ...prev,
      system: 0,
    }));
  }

  console.log("key:" + key);
};

return (
  <Tabs
        aria-label="Notifications"
        color="primary"
        fullWidth={true}
        selectedKey={selected}
        onSelectionChange={handleSelectionChange}
      >
        <Tab
          key="all"
          title={
            <Badge
              color="danger"
              content={unread.all}
              isInvisible={unread.all == 0}
            >
              <div className="flex items-center space-x-2">
                <span className="text-base font-medium">All</span>
                <BellAlertIcon className="h-6 w-6" />
              </div>
            </Badge>
          }
        >
          <ul>
            {notifications.map((n) => (
              <li
                key={n.id}
                className="p-2 border-b hover:bg-gray-50 cursor-pointer"
              >
                {n.text}
              </li>
            ))}
          </ul>
        </Tab>
        <Tab
          key="mentions"
          title={
            <Badge
              color="danger"
              content={unread.mentions}
              isInvisible={unread.mentions == 0}
            >
              <div className="flex items-center space-x-2">
                <span className="text-base font-medium">Mentions</span>
                <UserGroupIcon className="h-6 w-6" />
              </div>
            </Badge>
          }
        >
          <p>Mentions notifications go here</p>
        </Tab>
        <Tab
          key="system"
          title={
            <Badge
              color="danger"
              content={unread.system}
              isInvisible={unread.system == 0}
            >
              <div className="flex items-center space-x-2">
                <span className="text-base font-medium">System</span>
                <Cog6ToothIcon className="h-6 w-6" />
              </div>
            </Badge>
          }
        >
          <p>System notifications go here</p>
        </Tab>
      </Tabs>
);
Clear badge

Making It Production Ready

Before shipping, let’s polish the experience:

  • Persist State — Store read/unread status in a database or local storage.
  • Real-Time Updates — Use WebSockets, Pusher, or Next.js server actions.
  • Responsive Design — Ensure Tabs and Toasts look great on mobile.
  • Accessibility — HeroUI already ships with ARIA roles, but always test with screen readers to ensure optimal accessibility.

This ensures your Notification Center isn’t just pretty but also usable at scale.

Wrapping Up

You’ve built a Slack-style Notification Center in Next.js using HeroUI Tabs, Badges, and Toasts.

We covered:

  • Setting up HeroUI with Next.js.
  • Organizing notifications into Tabs.
  • Using Badges for unread counts.
  • Triggering Toasts for real-time feedback.
  • Syncing counts and messages together.

With these steps, your app now feels more engaging and user-friendly.

Keep experimenting — add categories, animations, or even push notifications. Users will love the clarity and polish this brings.

This article was originally published on Medium.

Leave a Comment

Your email address will not be published. Required fields are marked *