Notification UI Components

Use the NotificationsPopover component or build custom notification UIs with the provided React hooks.

MakerKit provides a ready-to-use NotificationsPopover component and React hooks for building custom notification interfaces.

NotificationsPopover

The default notification UI: a bell icon with badge that opens a dropdown list.

import { NotificationsPopover } from '@kit/notifications/components';
function AppHeader({ accountId }: { accountId: string }) {
return (
<header>
<NotificationsPopover
accountIds={[accountId]}
realtime={false}
/>
</header>
);
}

Props

PropTypeRequiredDescription
accountIdsstring[]YesAccount IDs to fetch notifications for
realtimebooleanYesEnable Supabase Realtime subscriptions
onClick(notification) => voidNoCustom click handler

How accountIds works

Pass all account IDs the user has access to. For a user with a personal account and team memberships:

import { useUserWorkspace } from '@kit/accounts/hooks/use-user-workspace';
function NotificationsWithAllAccounts() {
const { account, accounts } = useUserWorkspace();
// Include personal account + all team accounts
const accountIds = [
account.id,
...accounts.filter(a => !a.is_personal_account).map(a => a.id)
];
return (
<NotificationsPopover
accountIds={accountIds}
realtime={false}
/>
);
}

The built-in layouts handle this automatically. You only need to configure accountIds for custom implementations.

Custom click handling

By default, clicking a notification with a link navigates using an anchor tag. Override this with onClick:

import { useRouter } from 'next/navigation';
function CustomNotifications({ accountId }: { accountId: string }) {
const router = useRouter();
return (
<NotificationsPopover
accountIds={[accountId]}
realtime={false}
onClick={(notification) => {
if (notification.link) {
// Custom navigation logic
router.push(notification.link);
}
}}
/>
);
}

What the component renders

  • Bell icon with red badge showing unread count
  • Popover dropdown with notification list on click
  • Each notification shows:
    • Type icon (info/warning/error with color coding)
    • Message body (truncated at 100 characters)
    • Relative timestamp ("2 minutes ago", "Yesterday")
    • Dismiss button (X icon)
  • Empty state when no notifications

The component uses Shadcn UI's Popover, Button, and Separator components with Lucide icons.

React hooks

Build custom notification UIs using these hooks from @kit/notifications/hooks.

useFetchNotifications

Fetches initial notifications and optionally subscribes to real-time updates.

'use client';
import { useState, useCallback } from 'react';
import { useFetchNotifications } from '@kit/notifications/hooks';
type Notification = {
id: number;
body: string;
dismissed: boolean;
type: 'info' | 'warning' | 'error';
created_at: string;
link: string | null;
};
function CustomNotificationList({ accountIds }: { accountIds: string[] }) {
const [notifications, setNotifications] = useState<Notification[]>([]);
const onNotifications = useCallback((newNotifications: Notification[]) => {
setNotifications(prev => {
// Deduplicate by ID
const existingIds = new Set(prev.map(n => n.id));
const unique = newNotifications.filter(n => !existingIds.has(n.id));
return [...unique, ...prev];
});
}, []);
useFetchNotifications({
accountIds,
realtime: false,
onNotifications,
});
return (
<ul>
{notifications.map(notification => (
<li key={notification.id}>{notification.body}</li>
))}
</ul>
);
}

Parameters:

ParameterTypeDescription
accountIdsstring[]Account IDs to fetch for
realtimebooleanSubscribe to real-time updates
onNotifications(notifications: Notification[]) => voidCallback when notifications arrive

Behavior:

  • Fetches up to 10 most recent non-dismissed, non-expired notifications
  • Uses React Query with refetchOnMount: false and refetchOnWindowFocus: false
  • Calls onNotifications with initial data and any real-time updates

useDismissNotification

Returns a function to dismiss (mark as read) a notification.

'use client';
import { useDismissNotification } from '@kit/notifications/hooks';
function NotificationItem({ notification }) {
const dismiss = useDismissNotification();
const handleDismiss = async () => {
await dismiss(notification.id);
// Update local state after dismissing
};
return (
<div>
<span>{notification.body}</span>
<button onClick={handleDismiss}>Dismiss</button>
</div>
);
}

The function updates the dismissed field to true in the database. RLS ensures users can only dismiss their own notifications.

Notification type

All hooks work with this type:

type Notification = {
id: number;
body: string;
dismissed: boolean;
type: 'info' | 'warning' | 'error';
created_at: string;
link: string | null;
};

Building a custom notification center

Full example combining the hooks:

'use client';
import { useState, useCallback } from 'react';
import {
useFetchNotifications,
useDismissNotification,
} from '@kit/notifications/hooks';
type Notification = {
id: number;
body: string;
dismissed: boolean;
type: 'info' | 'warning' | 'error';
created_at: string;
link: string | null;
};
export function NotificationCenter({ accountIds }: { accountIds: string[] }) {
const [notifications, setNotifications] = useState<Notification[]>([]);
const dismiss = useDismissNotification();
const onNotifications = useCallback((incoming: Notification[]) => {
setNotifications(prev => {
const ids = new Set(prev.map(n => n.id));
const newOnes = incoming.filter(n => !ids.has(n.id));
return [...newOnes, ...prev];
});
}, []);
useFetchNotifications({
accountIds,
realtime: true, // Enable real-time
onNotifications,
});
const handleDismiss = async (id: number) => {
await dismiss(id);
setNotifications(prev => prev.filter(n => n.id !== id));
};
if (notifications.length === 0) {
return <p>No notifications</p>;
}
return (
<div className="space-y-2">
{notifications.map(notification => (
<div
key={notification.id}
className="flex items-center justify-between p-3 border rounded"
>
<div>
<span className={`badge badge-${notification.type}`}>
{notification.type}
</span>
{notification.link ? (
<a href={notification.link}>{notification.body}</a>
) : (
<span>{notification.body}</span>
)}
</div>
<button onClick={() => handleDismiss(notification.id)}>
Dismiss
</button>
</div>
))}
</div>
);
}

This gives you full control over styling and behavior while leveraging the built-in data fetching and real-time infrastructure.