3 unread notifications

Keyboard

↑↓
Move between notifications
HomeEnd
First or last notification
Escape
Close and return focus

The CLI also installs the npm packages and registry items it builds on, and reads the style in your components.json to pick the Base UI or Radix build.

notification-inbox-usage.tsx
"use client"

import * as React from "react"
import { ArchiveIcon } from "lucide-react"

import { NotificationInbox, type InboxNotification } from "@/components/ui/notification-inbox"

export function Inbox({ initial }: { initial: InboxNotification[] }) {
  const [items, setItems] = React.useState(initial)

  return (
    <NotificationInbox
      items={items}
      onItemsChange={setItems}
      onMarkRead={(ids) => fetch("/api/notifications/read", { method: "POST", body: JSON.stringify({ ids }) })}
      onMarkAllRead={() => fetch("/api/notifications/read-all", { method: "POST" })}
      actions={[
        {
          label: "Archive",
          icon: <ArchiveIcon className="size-3.5" />,
          onSelect: (n) => setItems((current) => current.filter((item) => item.id !== n.id)),
        },
      ]}
    />
  )
}

NotificationInbox

A bell button that shows the unread count and opens notifications grouped by day, newest first. Up and Down arrows, Home and End move between items, and Escape closes the popover.

Prop
itemsT[]Controlled notifications. T extends InboxNotification, so extra fields reach renderItem and actions typed.
defaultItemsT[]Default[]Initial notifications when uncontrolled.
onItemsChange(items: T[]) => voidCalled with the full list when read state changes: an unread item is opened, an item is marked read or unread, or Mark all as read runs. Opening a read item changes nothing, so track opens with onItemClick.
onMarkRead(ids: string[]) => voidCalled when an unread item is opened or its Mark as read button is pressed.
onMarkUnread(ids: string[]) => voidCalled when Mark as unread is pressed on a read item.
onMarkAllRead() => voidCalled by Mark all as read. onMarkRead is not called for it.
onItemClick(notification: T) => voidCalled when an item is opened, after it is marked read. The popover then closes.
actionsNotificationAction<T>[]Default[]Extra icon buttons on each row, each { label, icon, onSelect(notification) }. If onSelect removes the item, focus moves to the next item.
renderItem(notification: T) => ReactNodeReplaces the content of each row. It renders inside the row's button or link, so it must not contain interactive elements. Defaults to NotificationItem.
openbooleanControlled open state of the popover.
defaultOpenbooleanDefaultfalseInitial open state when uncontrolled.
onOpenChange(open: boolean) => voidCalled when the popover opens or closes.
nowDateReference time for the Today and Yesterday headings. Defaults to the current time when the popover renders.
localestringLocale for day headings and times. Defaults to the browser locale.
aria-labelstringDefault"Notifications"Panel heading and the trigger's accessible name, which adds the unread count.

Other props go to the <button> (the trigger).

InboxNotification

One notification.

Prop
idRequiredstringUnique id.
titleRequiredstringMain line. Also names the row's action buttons for screen readers.
bodystringSecondary text, clamped to two lines.
createdAtRequiredDate | stringAny value new Date() accepts. Sets the day group and the time. Items with an invalid date go last under Earlier.
readbooleanDefaultfalseUnread items show a dot and count toward the badge.
hrefstringRenders the row as a link instead of a button.
actor{ name: string; avatarUrl?: string }Shows an avatar, with initials when there is no avatarUrl or it fails to load.

NotificationItem

The default row content: avatar, title, body and time. Takes the InboxNotification fields, so <NotificationItem {...notification} /> works inside renderItem.

Prop
localestringLocale for the time.
classNamestringClasses for the content wrapper.

Controlled with actions

Own the items array and keep it in sync through onItemsChange. onMarkRead, onMarkUnread and onMarkAllRead report each change, for example to persist it, and actions add per-item buttons such as Archive.

2 unread notifications

Nothing yet

Custom item

renderItem replaces the content of each row. Extra fields on your items reach it typed, and NotificationItem renders the default title, body and time.

2 unread notifications
components/ui/notification-inbox.tsx
CtrlK