React components

The InboxButton and InboxPanel components, the useInbox hook and useSeenOnView, with styling and Tailwind setup.

The React entry point gives you a ready-made bell and inbox panel, a hook for your own UI, and a helper that marks rows seen. Import from @byo-oss/notifications/react.

InboxButton

<InboxButton /> is the bell, the unread badge and the inbox panel together:

components/navbar.tsx
import { InboxButton } from "@byo-oss/notifications/react";

<InboxButton client={authClient} onNavigate={(href) => router.push(href)} />

Props:

  • client (required): your auth client, with the inboxClient() plugin.
  • onNavigate: called with the notification’s href when it’s clicked.
  • renderItem: replaces how each notification row is drawn.
  • pollInterval: how often the unread count refreshes, in milliseconds. The default is 30 seconds. The poll pauses while the tab is hidden.
  • refreshOnFocus: a full refresh when the tab becomes visible. The default is true. The refresh is skipped when the data is under 5 seconds old. Opening the panel always refreshes.
  • filter: "unread" or "all". Passing it locks the list and hides the tabs.
  • organizationId: show only notifications for one org.
  • className: classes for the wrapper.
  • buttonClassName: classes for the trigger. This replaces the default look.
  • labels: replaces any of the English strings. See Labels.

The panel opens on an Unread tab, which is fetched server-side with filter: "unread". All is the second tab.

Matching your design system

InboxButton renders its own <button>, so don’t wrap it in another one. To match your design system’s trigger, pass buttonClassName. It replaces the default look. With shadcn:

components/navbar.tsx
import { buttonVariants } from "@/components/ui/button";

<InboxButton client={authClient} buttonClassName={buttonVariants({ variant: "ghost", size: "icon" })} />

Tailwind v4

Tailwind v4 skips node_modules when it scans for classes. Point it at the package from your CSS, or some of the button’s and panel’s classes won’t be generated:

app/globals.css
@import "tailwindcss";
@source "../node_modules/@byo-oss/notifications/dist"; /* path relative to this CSS file */

InboxPanel and useInbox

<InboxPanel /> is the panel without the bell. It takes the result of useInbox:

components/inbox.tsx
import { InboxPanel, useInbox } from "@byo-oss/notifications/react";

<InboxPanel inbox={useInbox(authClient)} />

Pass view and onViewChange to control its tabs. Uncontrolled, it defaults to Unread.

useInbox(client, options) builds your own UI. It returns:

{ notifications, unreadCount, isLoading, hasMore, loadMore, markRead, markSeen, markAllRead, refresh, refreshIfStale }

Options: pollInterval, pageSize, filter, organizationId, refreshOnFocus.

refresh() always fetches. Concurrent calls share one request. refreshIfStale() skips the fetch if the data is under 5 seconds old.

filter: "unread" fetches only unread rows, so a page is a full page:

ts
const inbox = useInbox(authClient, { filter: "unread", pageSize: 5 });

This yields five unread notifications. It doesn’t yield five rows that you then filter down to however many are unread. Rows you mark read stay in the list until the next refresh(). loadMore() accounts for them when paging.

Marking rows seen in a custom UI

In a custom UI, use useSeenOnView and attach its ref to each unread row. Then only rows people actually look at cancel pending emails:

components/inbox-list.tsx
const inbox = useInbox(authClient);
const seenRef = useSeenOnView(inbox.markSeen);

{inbox.notifications.map((n) => (
  <li key={n.id} ref={n.read ? undefined : seenRef(n.id)}>
    {n.title}
  </li>
))}

It uses IntersectionObserver. Where that’s missing, it marks nothing.

onNavigate receives the whole href. For TanStack Start or TanStack Router, pass the href to router.navigate({ href }). Mount the button in the root layout so navigations don’t remount it:

app/root.tsx
import { useRouter } from "@tanstack/react-router";

const router = useRouter();
<InboxButton client={authClient} onNavigate={(href) => router.navigate({ href })} />

A working app with an end-to-end suite is in examples/tanstack-start.

Labels

labels on InboxButton and InboxPanel replaces any of the English strings. The keys are notifications (the bell’s accessible name), unread, all, markAllRead, empty, loadMore, unreadCount(count) (announced to screen readers when the count changes), badge(count) (the count on the bell’s badge) and relativeTime(date) (a notification’s age under its title). Keys you leave out keep defaultInboxLabels.

components/navbar.tsx
<InboxButton client={authClient} labels={{ unread: "Ungelesen", all: "Alle", markAllRead: "Alle gelesen" }} />
Edit this page on GitHub ↗@byo-oss/notifications 0.11.4