Skip to content
kerfjsGitHub

Inbox

A person's notifications, by day, with tabs, actions and undo.

SourcePackage @kerf-js/veneer

Preview

tsx
import { state } from '@kerf-js/core'import { Inbox, Toaster } from '@kerf-js/veneer'import type { Note } from '@kerf-js/veneer'export function Page() {  const inbox = useInbox()  return (    <div style="block-size:520px">      <Inbox        title="Notifications"        live="live"        {...inbox}      />      <Toaster />    </div>  )}const NOTES: Note[] = [  {    id: 'n1',    actor: 'Sam Okafor',    kind: 'mention',    verb: 'mentioned you in',    object: 'mobile-bff',    time: '9 min',    day: 'Today',    source: 'Deploys',    quote:      'Can you look at the retry ' +      'settings before we redeploy?',  },  {    id: 'n2',    actor: 'Jun Tanaka',    kind: 'approve',    verb: 'asked you to approve',    object: 'Release 2026.11',    time: '32 min',    day: 'Today',    actions: [      { id: 'approve', label: 'Approve' },      { id: 'view', label: 'View', emphasis: 'outline' },    ],  },  {    id: 'n3',    actor: 'kerf',    kind: 'system',    verb: 'failed the health check on',    object: 'd-4819',    detail: 'in ap-south',    time: '1 h',    day: 'Today',  },  {    id: 'n4',    actor: 'Mara Lindqvist',    kind: 'invite',    verb: 'invited you to',    object: 'Incident review',    detail: '· 13:00',    time: '2 h',    day: 'Today',    source: 'Calendar',    read: true,  },  {    id: 'n5',    actor: 'Priya Raman',    kind: 'comment',    verb: 'commented on',    object: 'storefront',    time: 'Yesterday',    day: 'Yesterday',    quote: 'The edge cache cut image load times in half.',    read: true,  },  {    id: 'n6',    actor: 'Lena Fischer',    kind: 'assign',    verb: 'assigned you',    object: 'Ranking v2 handover',    time: 'Yesterday',    day: 'Yesterday',    read: true,  },]const min = 60_000function useInbox() {  const items = state(NOTES)  const change = (id: string, patch: Partial<Note>) =>    (items.value = items.value.map((n) =>      n.id === id ? { ...n, ...patch } : n,    ))  return {    items,    onRead: (id: string) => change(id, { read: true }),    onReadAll: () =>      (items.value = items.value.map((n) =>        n.read ? n : { ...n, read: true },      )),    onDismiss: (id: string) =>      (items.value = items.value.filter(        (n) => n.id !== id,      )),    onRestore: (note: Note) =>      (items.value = [note, ...items.value]),    onAction: (note: Note, action: string) =>      change(note.id, {        read: true,        actions: undefined,        detail:          action === 'approve'            ? '· approved'            : note.detail,      }),  }}

Installation

npm install @kerf-js/veneer

Usage

tsx
import { Inbox } from '@kerf-js/veneer'

First load

items stays undefined until it arrives.

tsx
import { Inbox } from '@kerf-js/veneer'export function Loading() {  return (  <Inbox items={undefined} />  )}

Empty

An Inbox with no items shows its empty state.

tsx
import { Inbox } from '@kerf-js/veneer'import type { Note } from '@kerf-js/veneer'export function Empty() {  return (  <Inbox items={NOTHING} />  )}const NOTHING: Note[] = []

When to use

Use it for

  • Things that need someone's attention

Reach for something else

Accessibility

LeftandRight
Move between the tabs
UpandDown
Move between notifications
  • Rows are a feed, keyed, so a change patches one row and focus stays where it was.
  • It needs a mounted Toaster for Undo.

API Reference

PropTypeDefaultDescription
itemsNote[] | { value: Note[] | undefined; } | (() => Note[] | undefined)—Newest first. Undefined while loading.
liveLiveState | (() => LiveState)—The connection's state, shown in the foot as "Up to date" or "Connecting…".
tabsInboxTab[]—The filter tabs, in order. Default all, unread and mentions.
titlestring—The heading.
variant"popover"—popover for inside a Bell.
openbooleanfalseIs it on screen? Rows are only marked read while it is.
queuednumber—Updates waiting to sync.
viewAllstring—A link to the full page.
settings(() => void)—Adds a settings button to the header, and is called when it is pressed.
EventTypeDefaultDescription
onRead((id: string) => void)—Called with a row's id once it has been seen or opened.
onReadAll(() => void)—Called when Mark all as read is pressed.
onDismiss((id: string) => void)—Called with a row's id when it is dismissed.
onRestore((note: Note) => void)—Undo a dismiss: put the row back.
onAction((note: Note, action: string) => void)—A row's button was pressed.
onOpen((note: Note) => void)—Makes each row a link.

Parts

Every element carries a stable name, so you can style it from your own CSS: [data-scope="inbox"] [data-part="root"]

rootnew-pillfooter

RelatedBellActivity feedNotification preferences