# Notification Center

> A React notification center: a bell with an unread count that opens an inbox with tabs, day groups, inline actions and swipe to dismiss.

- Type: React component (Feedback)
- Access: Pro (the source needs a Wingo UI Pro plan, see https://wingo-ui.com/pricing)
- Version: 1.0.0
- Install: `npx wingo-ui@latest add notification-center`
- Docs: https://wingo-ui.com/components/notification-center

## Interaction

Click the bell (or press its shortcut) and the panel grows out of it with All / Unread / Mentions tabs, the Unread tab counting live. Notifications sit under sticky day labels (Today, Yesterday, weekdays, then dates) with relative times that refresh every minute. Click a row to open it: it turns read, follows its link and the panel closes. Inline buttons (accept an invite, retry a payment) spin while their promise runs. The ⋯ menu on each row (on hover or focus with a mouse, always visible on touch) marks it read or unread or dismisses it, and the row collapses out. "Mark all as read" clears every dot at once and the red count on the bell scales away; when a new notification arrives the bell swings and the row slides in, and screen readers hear it through a polite live region. Keyboard: Tab into the panel, Down or Home jumps to the first row, Up / Down move between rows, Home / End jump, Enter opens, Escape closes and focus returns to the bell. The last row loads more by itself when it scrolls into view. On a phone the panel is a full-height bottom sheet: swipe a row left to dismiss it (a red Dismiss layer follows your finger) or right to toggle read, with a haptic tick where supported; drag the handle down to close.

## Installation

```bash
npx wingo-ui@latest add notification-center
```

Pro item: run `npx wingo-ui@latest login` once with a Pro account. The CLI adds the files, their dependencies and a version record for updates.

With shadcn: `npx shadcn@latest add @wingo-ui/notification-center` (needs the @wingo-ui registry with your API key in components.json)

Files:

- `components/ui/notification-center.tsx`

npm dependencies: `npm i @radix-ui/react-popover lucide-react motion`

Wingo UI dependencies (installed automatically):

- [Avatar](https://wingo-ui.com/components/avatar) ([Markdown](https://wingo-ui.com/components/avatar.md))
- [Badge](https://wingo-ui.com/components/badge) ([Markdown](https://wingo-ui.com/components/badge.md))
- [Button](https://wingo-ui.com/components/button) ([Markdown](https://wingo-ui.com/components/button.md))
- [Drawer](https://wingo-ui.com/components/drawer) ([Markdown](https://wingo-ui.com/components/drawer.md))
- [Dropdown Menu](https://wingo-ui.com/components/dropdown-menu) ([Markdown](https://wingo-ui.com/components/dropdown-menu.md))
- [Skeleton](https://wingo-ui.com/components/skeleton) ([Markdown](https://wingo-ui.com/components/skeleton.md))
- [Spinner](https://wingo-ui.com/components/spinner) ([Markdown](https://wingo-ui.com/components/spinner.md))
- [Tabs](https://wingo-ui.com/components/tabs) ([Markdown](https://wingo-ui.com/components/tabs.md))
- [useControllableState](https://wingo-ui.com/components/use-controllable-state) ([Markdown](https://wingo-ui.com/components/use-controllable-state.md))
- [useLocale](https://wingo-ui.com/components/use-locale) ([Markdown](https://wingo-ui.com/components/use-locale.md))
- [useMediaQuery](https://wingo-ui.com/components/use-media-query) ([Markdown](https://wingo-ui.com/components/use-media-query.md))
- [useShortcut](https://wingo-ui.com/components/use-shortcut) ([Markdown](https://wingo-ui.com/components/use-shortcut.md))
- [Date utils](https://wingo-ui.com/components/date-utils) ([Markdown](https://wingo-ui.com/components/date-utils.md))
- [Motion presets](https://wingo-ui.com/components/motion) ([Markdown](https://wingo-ui.com/components/motion.md))
- [Navigation](https://wingo-ui.com/components/navigation) ([Markdown](https://wingo-ui.com/components/navigation.md))
- [Tones](https://wingo-ui.com/components/tones) ([Markdown](https://wingo-ui.com/components/tones.md))
- [UIProvider](https://wingo-ui.com/components/ui-config) ([Markdown](https://wingo-ui.com/components/ui-config.md))

## Usage

```tsx
import { CreditCard } from "lucide-react"
import { NotificationCenter, NotificationList, type NotificationItem } from "@/components/ui/notification-center"

const notifications: NotificationItem[] = [
  {
    id: "n1",
    type: "mention",
    name: "Sophia Lee",
    avatar: "/demo/ioana-dumitrescu.jpg",
    title: "Sophia Lee mentioned you in “Acme Corp quote”",
    body: "@Emma Carter can you check the discount before we send it?",
    createdAt: "2026-09-23T10:42:00",
    href: "/quotes/Q-2026-118",
  },
  {
    id: "n2",
    icon: <CreditCard />,
    tone: "danger",
    title: "Your Pro plan payment failed",
    body: "The card ending in 4821 was declined.",
    createdAt: "2026-09-23T08:15:00",
    actions: [{ label: "Retry", onSelect: () => fetch("/api/billing/retry", { method: "POST" }) }],
  },
]

export function TopbarNotifications() {
  return (
    <NotificationCenter
      notifications={notifications}
      onRead={(ids) => console.log("read", ids)}
      onDismiss={(id) => console.log("dismiss", id)}
    />
  )
}

// the panel without the bell (full inbox page, sidebar panel): same content props, a header when title is set.
// it paints on var(--background); on a raised panel set [--nc-surface:var(--surface)] in className
export function InboxPage() {
  return <NotificationList title="Notifications" notifications={notifications} className="rounded-3xl border border-border" />
}
```

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `notifications?` | `NotificationItem[]` | `"[]"` | Newest first. { id, title: ReactNode, body?, createdAt: Date \| string \| number, read?, type?, avatar?, name?, icon?, tone?, href?, actions?: NotificationAction[] }. NotificationAction is { label, onSelect: () => void \| Promise<unknown>, variant?: "solid" \| "soft" \| "ghost", tone? }. String dates are parsed; invalid ones go last without a time. type "mention" feeds the Mentions tab, and @Name inside string titles and bodies is highlighted. |
| `onRead?` | `(ids: string[]) => void` |  | Rows turned read: from the row menu, a swipe right, opening a row or Mark all (when onReadAll is not set). The panel updates at once and keeps the change until your data agrees. |
| `onUnread?` | `(ids: string[]) => void` |  | Rows turned unread from the row menu or a swipe right on a read row. |
| `onReadAll?` | `() => void` |  | "Mark all as read" in the header (shown while anything is unread). Without it onRead gets every unread id. |
| `onDismiss?` | `(id: string) => void` |  | A row was dismissed (row menu or swipe left). Runs after the row has collapsed out, so remove it from your data here. |
| `onSelect?` | `(item: NotificationItem) => void` |  | A row was opened: it turns read, navigates to its href through NavLink and the panel closes. |
| `filters?` | `NotificationFilter[]` |  | The tabs, All / Unread / Mentions by default: { value, label, filter?: (item) => boolean, badge?, emptyTitle?, emptyText? }. A tab whose value is "unread" shows the unread count; each tab can have its own empty state. Fewer than two filters hide the tabs. |
| `filter?` | `string` |  | The active tab, controlled. Pair it with onFilterChange. |
| `defaultFilter?` | `string` | `"all"` | The tab it starts on while uncontrolled (a filter value). |
| `onFilterChange?` | `(filter: string) => void` |  | Fires when another tab is picked. |
| `title?` | `ReactNode` | `"Notifications"` | The panel title; it also names the dialog. |
| `markAllLabel?` | `string` | `"Mark all as read"` | The header button that clears every unread dot. |
| `emptyTitle?` | `string` | `"You're all caught up"` | The empty state of a tab with nothing in it. A filter's own emptyTitle wins. |
| `emptyText?` | `string` | `""` | A second, muted line under the empty title. |
| `emptyIcon?` | `ReactNode` |  | Replaces the BellOff icon of the empty state. |
| `groupByDay?` | `boolean` | `true` | Sticky day labels: Today, Yesterday, weekday names this week, then dates in the locale. Off, rows show the date with the time. |
| `timeFormat?` | `"relative" \| "time"` | `"relative"` | "5 min ago" through Intl.RelativeTimeFormat, refreshed every minute while open, or the clock time ("14:20"). Under an older day label rows show the clock time, so "Yesterday" is never said twice. Relative times are computed after mount: the server and hydration render print the clock time, so there is no mismatch. |
| `tone?` | `"neutral" \| "brand" \| "success" \| "warning" \| "danger" \| "info"` | `"neutral"` | The unread dots and the tab indicator. |
| `badgeTone?` | `"neutral" \| "brand" \| "success" \| "warning" \| "danger" \| "info"` | `"danger"` | The count badge on the bell. Red by default on purpose: people read a red count as unread. |
| `max?` | `number` | `99` | Counts above it read "99+". |
| `width?` | `number` | `400` | The desktop panel width in px (never wider than the screen minus 24px). |
| `badgeRingColor?` | `string` | `"var(--bar-bg, var(--background))"` | The 2px ring that cuts the badge out of the bell: the surface the bell sits on. |
| `count?` | `number` | `""` | Overrides the badge number (default: the unread count). 0 hides the badge. The playground's empty control means left out. |
| `loading?` | `boolean` | `false` | Skeleton rows while the list is empty, a spinner row after the last one otherwise. |
| `hasMore?` | `boolean` | `false` | Shows the "Load more" row at the end, which also loads by itself when it scrolls into view. |
| `onLoadMore?` | `() => (void \| Promise<unknown>)` |  | Loads the next page. A returned promise keeps a spinner in the row until it settles. |
| `defaultOpen?` | `boolean` | `false` | Starts open (uncontrolled). |
| `open?` | `boolean` |  | Controlled open state. Pair it with onOpenChange. |
| `onOpenChange?` | `(open: boolean) => void` |  | Fires when the panel opens or closes. |
| `ringOnNew?` | `boolean` | `true` | The bell swings from its top when the unread count goes up (never under reduced motion). |
| `swipeActions?` | `boolean` | `true` | Swipe a row left to dismiss it, right to toggle read. Follows the finger and commits past a quarter of the row or on a flick. |
| `responsive?` | `boolean` | `true` | Below 768px the panel is a full-height bottom sheet with full-width tabs. |
| `side?` | `"top" \| "right" \| "bottom" \| "left"` | `"bottom"` | Where the desktop panel opens; it flips when there is no room. |
| `align?` | `"start" \| "center" \| "end"` | `"end"` | The panel's alignment against the bell. end suits a bell at the right of a top bar. |
| `shortcut?` | `string` | `""` | A key combo in mod+k notation that opens and closes the panel from anywhere (none by default). |
| `locale?` | `string` | `""` | Dates, weekdays and relative times. Empty uses the LocaleProvider or the browser. |
| `settingsHref?` | `string` | `""` | Adds a gear link in the header (a NavLink), e.g. to notification settings. |
| `onSettings?` | `() => void` |  | Adds a gear button in the header, or runs before the settingsHref link. |
| `trigger?` | `ReactNode` |  | A custom trigger instead of the bell, rendered with asChild (one element that takes a ref). |
| `linkComponent?` | `(props: LinkComponentProps) => ReactNode` |  | The router link for rows with href and the settings link; default from LinkProvider. |
| `container?` | `HTMLElement \| null` |  | Portal target for the panel and the sheet (default document.body). |
| `labels?` | `{ trigger?: (unread) => string; markRead?; markUnread?; dismiss?; more?; loadMore?; loading?; settings?; filters?; unread?; earlier?; newNotification?: (title) => string }` |  | Every other string, English by default: the bell's label ("Notifications, 3 unread"), the row menu, the swipe layers, load more, the live announcement for new notifications. |
| `classNames?` | `Partial<Record<"trigger" \| "badge" \| "content" \| "header" \| "title" \| "markAll" \| "settings" \| "tabs" \| "list" \| "group" \| "groupLabel" \| "item" \| "media" \| "body" \| "time" \| "actions" \| "empty" \| "footer", string>>` |  | Extra classes for the inner parts. |
| `className?` | `string` |  | Extra classes for the bell button. Other props (aria-*, data-*, handlers) go to the bell too. |

## Parts

### NotificationList (component)

The inbox without the bell, for a full notifications page or a sidebar panel: the same tabs, day groups, rows, actions, empty state and load more. Takes every list prop of NotificationCenter (notifications, filters, onRead, onDismiss, labels...) plus div props.

### NotificationItem (type)

One notification: { id, title, body?, createdAt, read?, type?, avatar?, name?, icon?, tone?, href?, actions? }. type "mention" feeds the Mentions tab.

### NotificationAction (type)

An inline button on a row: { label, onSelect, variant?, tone? }. A returned promise keeps it spinning until it settles.

### NotificationFilter (type)

A tab: { value, label, filter?, badge?, emptyTitle?, emptyText? }.

## Examples

- **Phone sheet with swipe actions**: The full-height sheet a phone gets: swipe left to delete, right to mark read.
- **Empty states per tab**: Each filter brings its own empty title and text; everything here is already read.
- **Custom filters**: All / Invoices / Team with a count on the invoices tab.
- **Inbox page**: NotificationList on its own, with a settings link and pages that load as you scroll.
- **Time format and locale**: Relative US times next to 24-hour British clock times.
- **Tones**: Brand and info unread dots and badges on a raised bar.
- **Loading**: Skeleton rows on the first load, then the list fades in.

## See also

- [Notification Bell](https://wingo-ui.com/components/notification-bell) ([Markdown](https://wingo-ui.com/components/notification-bell.md))
- [Badge](https://wingo-ui.com/components/badge) ([Markdown](https://wingo-ui.com/components/badge.md))
- [Tabs](https://wingo-ui.com/components/tabs) ([Markdown](https://wingo-ui.com/components/tabs.md))
- [Popover](https://wingo-ui.com/components/popover) ([Markdown](https://wingo-ui.com/components/popover.md))
- [Drawer](https://wingo-ui.com/components/drawer) ([Markdown](https://wingo-ui.com/components/drawer.md))

---

Canonical: https://wingo-ui.com/components/notification-center
More components: https://wingo-ui.com/components · Index for agents: https://wingo-ui.com/llms.txt
