Bell
The notifications button in the app bar, with a count of what is unread.
SourcePackage @kerf-js/veneer
Preview
tsx
import { derived, state } from '@kerf-js/core'import { Bell, Inbox, Toaster } from '@kerf-js/veneer'import type { Note } from '@kerf-js/veneer'export function WithInbox() { const inbox = useInbox() const unread = derived( () => inbox.items.value.filter((n) => !n.read).length, ) return ( <> <Bell count={unread} inbox={(open) => ( <Inbox variant="popover" {...inbox} open={open.value} /> )} /> <Toaster /> </> )}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/veneerUsage
tsx
import { Bell } from '@kerf-js/veneer'New arrivals
The count bumps once for each batch. Opening clears it here.
tsx
import { state } from '@kerf-js/core'import { Bell, Button, Row } from '@kerf-js/veneer'export function Arrivals() { const count = state(3) return ( <Row gap="related"> <Bell count={count} onOpen={() => (count.value = 0)} /> <Button emphasis="outline" tone="neutral" size="sm" onClick={() => count.value++} > A deploy failed </Button> </Row> )}Accessibility
- Its name says the count. The badge itself is hidden from assistive technology.
API Reference
| Prop | Type | Default | Description |
|---|---|---|---|
count | number | { value: number; } | (() => number | undefined) | — | A number, a state or a getter. |
inbox | ((open: State<boolean>) => unknown) | — | The panel. Gets open, a state. |
route | (() => void) | — | On a phone, called instead of opening. |
side | Side | 'bottom' | Which edge it sits on. |
align | Align | 'end' | How it lines up with what opened it. |
| Event | Type | Default | Description |
|---|---|---|---|
onOpen | (() => void) | — | It was pressed. |
Parts
Every element carries a stable name, so you can style it from your own CSS: [data-scope="bell"] [data-part="content"]
content
RelatedInboxConnection pill