Notifications
betaA bell with the count of what's new, and the list it opens, grouped by day, with Mark all as read.
Preview
new
all read
inline
Notifications
empty
Notifications
You're all caught up.
Install
1. Add the registry to
components.json, once per app. Without it, the shadcn CLI looks up@txbrendanin a public list that txbrendan/ui does not control.npx shadcn@latest registry add "@txbrendan=https://txbrendan.dev/r/{name}.json"2. Add the theme, once per app. It replaces your color tokens with ones checked for contrast.
npx shadcn@latest add @txbrendan/theme3. Add the component.
npx shadcn@latest add @txbrendan/notifications
Use it for
- The bell in an app's header: what happened elsewhere while you were away, like an approval or a meeting moved.
- Give each item a title for what happened, the person who did it (or an icon), and meta for when and where: "18:04 · Design".
- Group items with group: "Today", "Yesterday", "Earlier". Groups show in the order they first appear.
- Opening one marks it read, closes the list, and calls onOpenItem. Take people to what it is about there.
- Mark all as read clears the dots at once, with Undo for 8 seconds. ⌘Z takes it back too. onReadAll runs when the window ends.
- Use inline to draw the open list in a card or a doc, with no bell.
Avoid it for
- News with a place on the screen. Show it there, like a status on its row.
- The result of something you just did. Show it on the control you pressed.
- Messages people reply to. Use a messages list.
Props
- items
Required. { id, title, person?, photo?, icon?: ReactNode, meta?, group?, read? }.
NotificationItem[]
- onOpenItem
Opens what it is about.
(item: NotificationItem) => void
- onRead
Marked read by opening it.
(ids: string[]) => void
- onReadAll
Mark all as read, once its 8-second undo window ends.
(ids: string[]) => void
- inline
Draws the open list in the page's flow, with no bell.
boolean · default false
- className
Extra classes on the bell, or on the list with inline. Later classes win.
string
| Prop | Type | Default | What it does |
|---|---|---|---|
| items | NotificationItem[] | - | Required. { id, title, person?, photo?, icon?: ReactNode, meta?, group?, read? }. |
| onOpenItem | (item: NotificationItem) => void | - | Opens what it is about. |
| onRead | (ids: string[]) => void | - | Marked read by opening it. |
| onReadAll | (ids: string[]) => void | - | Mark all as read, once its 8-second undo window ends. |
| inline | boolean | false | Draws the open list in the page's flow, with no bell. |
| className | string | - | Extra classes on the bell, or on the list with inline. Later classes win. |
Accessibility
- The bell is a button named with the count, like "Notifications, 3 new". It says it opens a dialog.
- The list is a dialog named "Notifications". Tab moves through Mark all as read, Close, and the rows, and wraps inside the list, even when pressed fast. The rows carry tabindex="0", so Safari keeps them in Tab too. Esc or Close shuts it and puts focus back on the bell.
- Each row is a button. A new one ends with a dot, which screen readers hear as "New", and forced-colors mode draws as a ring.
- Mark all as read says how many it marked in a polite live region. Its Undo undoes only once 1.2 seconds have passed, so a double press never undoes.
Examples
In a header
import { CalendarDays } from "lucide-react";
<Notifications
items={[
{ id: "1", person: "Kate Clarke", title: "Kate Clarke agreed to the launch plan", meta: "18:04 · Design", group: "Today" },
{ id: "2", icon: <CalendarDays />, title: "Saturday's workshop moved to 11:00", meta: "Calendar", group: "Yesterday", read: true },
]}
onOpenItem={(item) => console.log("open", item.id)}
/>