Notification Center
Collect notifications behind a bell, with unread counts, filters and actions.
import { NotificationCenter, NotificationItem } from '@youj/react'
Installation & usage
npm i @youj/reactFor an app already set up with npx @youj/cli init, which installs the peers (@youj/core, @youj/motion, motion and Base UI) and wires the provider. New app? Start at Installation.
Use a notification center for things that happened while someone was away and that they'll act on later: mentions, invites, deploys, alerts. Press the bell to open it, or simulate an arrival and watch the bell swing and the count roll. For a message about something that just happened in front of the person, use a Toast. For a history of events on one object, use a Timeline.
import { NotificationCenter, type NotificationItem } from '@youj/react'
export function Inbox({ items }: { items: NotificationItem[] }) {
return (
<NotificationCenter
notifications={items}
onMarkRead={(id) => markRead(id)}
onMarkAllRead={() => markAllRead()}
/>
)
}The center reads the list you pass and never changes it: marking read calls onMarkRead or onMarkAllRead, and you pass back the updated list. Each item has an id, a title and a time, and optionally:
actor, the person who did it (their avatar shows beside it), or aniconfor system events.body, a second line, clamped to two lines.read. Unread items carry a dot and count towards the badge.mention, which lists the item under Mentions.actions, buttons under the item, such as Reply or Accept.summary, plain text for announcements and toasts whentitleisn't a string.
It opens in a Popover aligned to the bell's end on desktop, and in a bottom Sheet below the md breakpoint.
Arrivals and toasts
A notification whose id wasn't in the previous list is an arrival. If it's unread, the bell swings once and it's announced. With the panel open, it takes its slot at the top of the list and the rest glide down.
useNotificationToasts also shows a Toast for each arrival. Pass enabled: !open so arrivals toast only while the panel is closed, and render one <Toaster /> in the app, as the Demo story does.
import { useState } from 'react'
const [open, setOpen] = useState(false)
useNotificationToasts(items, { enabled: !open })
<NotificationCenter notifications={items} open={open} onOpenChange={setOpen} />Description
A bell in a header with an inbox of mentions, deployments and invitations, and a button that simulates a new notification.
A bell with no notifications, showing the empty state.
Each tab has its own empty state: "No notifications", "You're all caught up" once everything is read, and "No mentions".
Code
notification-center-demo.tsx, the Demo story. The component itself comes from the package.
Reference
Propsnotifications, onMarkRead, onMarkAllRead… (8)
Props
| Prop | Type | |
|---|---|---|
notificationsRequired | readonly NotificationItem[] | – |
onMarkRead | (id: string) => void | – |
onMarkAllRead | () => void | – |
open | boolean | – |
onOpenChange | (open: boolean) => void | – |
locale | stringDefault: the YoujProvider locale. | – |
now | Date | numberThe moment Today and relative times count from, pinned (a demo's clock, a test). Default: the clock when the list opens. | – |
className | string | – |
useNotificationToasts(items, { enabled }) takes the same list, and toasts every arrival while enabled (default true).
MotionBell swing, count, badge… · reduced motion
Motion
| Moment | Tokens | ||
|---|---|---|---|
| Bell swingThe Youj BellIcon swings once per arrival; a second arrival mid-swing extends it | duration.deliberate560ms | 560ms | |
Countthe number rolls to its new value (Counter, roll) | duration.slowease.standard340ms · cubic-bezier(0.2, 0, 0, 1) | 340mscubic-bezier(0.2, 0, 0, 1) | |
| Badgepops in from scale 0 on the first unread | duration.baseease.enter220ms · cubic-bezier(0.16, 1, 0.3, 1) | 220mscubic-bezier(0.16, 1, 0.3, 1) | |
| Arrivalrises into its slot at the top and fades in (PresenceList) | duration.baseease.enter220ms · cubic-bezier(0.16, 1, 0.3, 1) | 220mscubic-bezier(0.16, 1, 0.3, 1) | |
| Removalfades out, as when an item leaves the Unread tab | duration.fastease.exit140ms · cubic-bezier(0.4, 0, 1, 1) | 140mscubic-bezier(0.4, 0, 1, 1) | |
| Reflowthe rest of the list glides to its new place | spring.smooth0.36s, bounce 0 | 0.36s, bounce 0 | |
| Unread dotfades and shrinks to half when read | duration.baseease.standard220ms · cubic-bezier(0.2, 0, 0, 1) | 220mscubic-bezier(0.2, 0, 0, 1) | |
| Tab indicatorglides to the chosen filter (SlidingIndicator) | spring.smooth0.36s, bounce 0 | 0.36s, bounce 0 |
Reduced motion
The bell doesn't swing and the badge doesn't pop; the count and the list change in place.
Reduced motion
The bell stays still, the badge appears without the pop and new or removed items change the list in place. Arrivals are still announced, and toasts still show.
AccessibilityKeyboard, focus, announcements
Accessibility
- The bell is a button whose name carries the count: "Notifications, 3 unread".
- Arrivals are announced politely: "New notification: Mira Chen mentioned you in Q3 roadmap".
- Unread items start with "Unread:" for screen readers, so the dot isn't the only signal.
- Mark as read, Mark all as read and every action are real buttons. Mark as read shows on hover and focus, and always on touch screens.
- Today and Earlier are labeled sections with their own headings, and the filters are Tabs with arrow-key navigation.
- Times are
<time>elements with a machine-readabledateTime.