Nasaq

NotificationCenter

Bell button with an unread badge that opens notifications in a popover or a full-height side panel, with All and Unread tabs, mark all read, a header slot and an empty state. Controlled.

PreviewOpen ↗

Code

import { NotificationCenter, type NotificationCenterItem } from "@fadymondy/nasaq/web";import { useState } from "react";const initial: NotificationCenterItem[] = [  { id: "1", actor: { name: "Sara Alharbi" }, title: "Sara mentioned you in MH-142", time: new Date(Date.now() - 300_000), unread: true },  { id: "2", title: "Invoice INV-031 was paid", time: "2026-09-28T09:00:00Z" },];export function HeaderBell() {  const [items, setItems] = useState(initial);  return (    <NotificationCenter      items={items}      onItemClick={(item) => setItems((all) => all.map((i) => (i.id === item.id ? { ...i, unread: false } : i)))}      onMarkAllRead={() => setItems((all) => all.map((i) => ({ ...i, unread: false })))}    />  );}

Alerts & Notifications · beta

Live examples and controls: NotificationCenter in the lab.

Install

npx shadcn@latest add https://docs.nasaqui.com/r/notification-center.json

The header bell. It shows an unread count, and opens a popover with a list of NotificationItem rows, All and Unread tabs, and a "Mark all read" action. It is controlled: it fetches nothing and keeps no read state. You pass items and handle onItemClick and onMarkAllRead.

When to use

  • The notifications entry in an app header, for a short list of recent items.
  • variant="sheet": a desktop-style notification centre that slides in from the edge, with room for a widget in header.

When not to use

  • A full inbox page or a long, paged history: use a page with Timeline or a table.
  • A side-over with notification settings: use Sheet with NotificationItem rows and NotificationPreferences.
  • A transient message: use a toast.

Import

import { NotificationCenter, type NotificationCenterItem } from "@fadymondy/nasaq/web";
// inside this monorepo: "@nasaq/web"

Quick start

import { NotificationCenter, type NotificationCenterItem } from "@fadymondy/nasaq/web";
import { useState } from "react";

const initial: NotificationCenterItem[] = [
  { id: "1", actor: { name: "Sara Alharbi" }, title: "Sara mentioned you in MH-142", time: new Date(Date.now() - 300_000), unread: true },
  { id: "2", title: "Invoice INV-031 was paid", time: "2026-09-28T09:00:00Z" },
];

export function HeaderBell() {
  const [items, setItems] = useState(initial);
  return (
    <NotificationCenter
      items={items}
      onItemClick={(item) => setItems((all) => all.map((i) => (i.id === item.id ? { ...i, unread: false } : i)))}
      onMarkAllRead={() => setItems((all) => all.map((i) => ({ ...i, unread: false })))}
    />
  );
}

Anatomy

NotificationCenter                 data-slot="notification-center"
├─ Popover trigger                 ghost icon Button, aria-label "Notifications, N unread"
│  └─ badge                        data-slot="notification-center-badge"   Num, "99+" above 99
└─ PopoverContent | SheetContent  data-variant="popover|sheet"
   ├─ header                       title + "Mark all read" Button
   ├─ header slot                  data-slot="notification-center-header", when `header` is set
   ├─ Tabs (underline)             All | Unread (with count)
   └─ panel                        <ul> of NotificationItem, or EmptyState

API

NotificationCenter

NotificationCenterProps extends Omit<ComponentProps<"div">, "children" | "title">.

PropTypeDefaultDescription
itemsreadonly NotificationCenterItem[]requiredThe notifications, newest first.
onItemClick?(item) => voidnoneA row was pressed. Mark it read here.
onMarkAllRead?() => voidnone"Mark all read" was pressed. The button is disabled with no unread.
unreadCount?numbercount of items with unreadUse when the server total exceeds the loaded items.
open? / defaultOpen? / onOpenChange?boolean / boolean / (open: boolean) => voiduncontrolledPopover or sheet state.
variant?"popover" | "sheet""popover"sheet opens a full-height side panel; the list fills its height.
header?ReactNodenoneShown under the title row, above the tabs: a date, weather or calendar widget.
labels?Partial<NotificationCenterLabels>locale stringsOverrides title, all, unread, markAllRead, emptyAll, emptyAllDescription, emptyUnread, emptyUnreadDescription, trigger(unread).
side? / align?Popover side / align, or "start" / "end""bottom" / "end"Popover placement. With variant="sheet", the edge the panel slides from (default end).

NotificationCenterItem

FieldTypeDescription
idstringStable key.
titleReactNodeHeadline.
description?ReactNodeSecondary line.
actor? / icon?{ name; avatar? } / ReactNodeLeading avatar or icon.
time?Date | number | stringShown as a relative time in the active locale (Latin digits, narrow).
unread?booleanAccent dot and stronger title.
href?stringRenders the row as a link.

Examples

Server-side count

import { NotificationCenter, type NotificationCenterItem } from "@fadymondy/nasaq/web";

export function Bell({ page, total }: { page: NotificationCenterItem[]; total: number }) {
  return <NotificationCenter items={page} unreadCount={total} />;
}

Overriding a label

import { NotificationCenter } from "@fadymondy/nasaq/web";

export function Bell() {
  return <NotificationCenter items={[]} labels={{ title: "التنبيهات" }} />;
}

Context menu

itemActions={(item) => ContextMenuAction[]} opens a menu on a row on right-click, Shift+F10 or the Menu key (mark read/unread, remove…). The popover stays open. contextMenu={false} opts out.

Accessibility

KeyAction
Enter / Space on the bellOpens the popover.
EscCloses it.
TabMoves through the tabs, the action and the rows.
Left / Right on a tabSwitches tab (follows reading direction).
  • The bell's accessible name includes the unread count; the visual badge is aria-hidden.
  • Unread rows have an sr-only "Unread" label, localised by the provider locale.
  • Rows are native buttons or links; the empty state is plain text.

RTL & i18n

  • The badge sits on the inline end of the bell, the popover aligns to the inline end, and rows use logical layout.
  • Counts go through Num; the cap renders as 99+.
  • Built-in English and Arabic strings, chosen by the provider locale.

Styling & tokens

  • Badge uses the accent variant; rows use hover and border tokens.
  • Target [data-slot=notification-center] and [data-slot=notification-center-badge]; extend with className. Never use raw hex.

Do / Don't

  • Do load a recent page and link to a full inbox.
  • Do update unread in your state in onItemClick.
  • Don't fetch inside the component; it is presentational.

Lab

https://docs.nasaqui.com/?path=/docs/components-alerts-notifications-notification-center--docs

On this page