Inbox
The notification Inbox — a header with actions, All | Unread chips, day groups of full-bleed rows, action chips, and empty, loading and error states.
- Status
- Since
0.23.4- Accessibility pattern
- named lists, row links, hover controls on focus
Last updated
Inbox
Unread: Asha assigned you Send Skyline delivery schedule
Skyline Tower B · Project
Unread: Notes ready for Lumen Build kickoff
Meeting · 4 action items
Unread: Raj assigned you 3 tasks3
Harbor Coffee · Customer
Processing failed for Weekly sync
Meeting · Try uploading the recording again
1,284 products
Install
Add Inbox from the VegaStack registry. The CLI verifies the item's integrity hash before writing it.
pnpm dlx shadcn@latest add @vegastack/inboxThe same command installs the registry items it composes: @vegastack/avatar, @vegastack/button, @vegastack/dropdown-menu, @vegastack/relative-time, @vegastack/skeleton, @vegastack/tooltip.
Usage
import {
Inbox,
InboxEmphasis,
InboxFilters,
InboxGroup,
InboxItem,
InboxMarkAllRead,
} from "@/components/ui/inbox";
<Inbox
onClose={close}
actions={<InboxMarkAllRead onClick={markAllRead} />}
filters={
<InboxFilters value={view} onValueChange={setView} unreadCount={3} />
}
onLoadMore={loadOlder}
hasMore={hasMore}
loadingMore={loadingOlder}
>
<InboxGroup label="Today">
<InboxItem
unread
avatar={{ name: "Asha Kumar" }}
title={
<>
<InboxEmphasis>Asha</InboxEmphasis> assigned you{" "}
<InboxEmphasis>Send Skyline delivery schedule</InboxEmphasis>
</>
}
meta="Skyline Tower B · Project"
time={createdAt}
href="/tasks/41"
onToggleRead={toggleRead}
/>
</InboxGroup>
</Inbox>;When to use
- Use it for the notifications panel: a feed of person events (someone assigned, mentioned or asked you) and system events (a meeting processed, an export finished, a job failed) that the user reads, acts on and marks read.
Inboxfills its container. Dock it in aSheet, aPopoveror a panel beside the sidebar; on a phone give that container the whole screen.- Don't use it for transient confirmations (Toast), a record's history (Timeline), or a bare unread count in the chrome (NotificationBell).
Anatomy
Inbox— the frame: title, header actions (InboxMarkAllRead,InboxMenuAction, anyInboxHeaderAction), the ✕ whenonCloseis set, thefiltersrow, a scrolling body and afooter— or, withonLoadMore, the infinite-scroll end.InboxFilters— the All | Unread chips, with the count on Unread.InboxGroup— a label that sticks within the scroll area, with no divider above or below it (16px above, except on the first group; 6px below), over a list it names.InboxListis the ungrouped list.InboxItem— one row. Title 14px regular — foreground when unread, muted once read — with the actor and record (InboxEmphasis) in medium; the tint marks unread. Meta 12px muted (the ramp has no 13px step), time 12px muted; 16px sides and 12px vertical padding; a 1px divider between rows only, and a tint, reaching both edges. The ⋯ menu sizes to its content (224–320px).InboxEmpty,InboxSkeleton,InboxError— the empty, first-load and failed-load states.
Examples
Person, system, actionable and grouped events
A person event leads with the actor's 28px avatar; a system event leads with its icon in a muted
28px tile (destructive for a failure). Emphasise the actor and the record with InboxEmphasis so
identical events name their record. actions adds up to three chips (the first can be primary),
each with loading and done states. count marks a row standing for grouped repeats ("Raj
assigned you 3 tasks"). Unread rows carry a soft tint and a foreground title; read titles are muted. A fixed right column
keeps the time on top; hover a row, or tab into it, to show Mark read / Mark unread and the ⋯ menu
below it.
Inbox
Unread: Asha assigned you Send Skyline delivery schedule
Skyline Tower B · Project
Unread: Notes ready for Lumen Build kickoff
Meeting · 4 action items
Unread: Raj assigned you 3 tasks3
Harbor Coffee · Customer
Processing failed for Weekly sync
Meeting · Try uploading the recording again
1,284 products
Infinite scroll
Pass onLoadMore, hasMore and loadingMore: a sentinel under the last row calls onLoadMore as
it nears the viewport (page 15 rows at a time). While a page loads, three skeleton rows stand in —
no spinner. Set loadMoreError after a failure to show a ghost Try again, and hasMore={false}
ends the list on "You’re all caught up".
Inbox
Unread: Asha assigned you Send Skyline delivery schedule
Skyline Tower B · Project
Inbox
1,284 products
Empty
Inbox
You’re all caught up
New assignments and meeting updates show up here.
Loading
Inbox
API Reference
| Prop | Type | Default | Description |
|---|---|---|---|
actions | React.ReactNode | — | Header icon buttons before the close button — usually InboxHeaderActions ("Mark all read", a settings menu). |
closeLabel | string | "Close inbox" | Accessible name of the close button. |
endLabel | React.ReactNode | "You’re all caught up" | The end of the list once hasMore is false. |
filters | React.ReactNode | — | The row under the title — usually InboxFilters. |
footer | React.ReactNode | — | Content under the list; with onLoadMore the end of the list is drawn for you. |
hasMore | boolean | false | More rows exist; false ends the list on endLabel. |
loadingMore | boolean | false | A page is loading: three skeleton rows show under the list. |
loadMoreError | boolean | false | The last page failed: a ghost "Try again" button replaces the sentinel and calls onLoadMore. |
onClose | (() => void) | — | Called by the ✕ button; omit to hide it. |
onLoadMore | (() => void) | — | Infinite scroll: called when the sentinel under the last row scrolls into view while
hasMore is true and nothing is loading. Load the next page (15 rows is the house size). |
title | React.ReactNode | "Inbox" | The heading. |
Data attributes and CSS variables on Inbox
| Attribute | Values |
|---|---|
data-slot | "inbox" | "inbox-body" |
InboxFilters
| Prop | Type | Default | Description |
|---|---|---|---|
onValueChange* | (value: InboxFilter) => void | — | Called with the filter a chip selects. |
value* | InboxFilter | — | The selected filter. |
className | string | — | |
unreadCount | number | 0 | The unread count shown on the Unread chip; 0 hides it. |
InboxGroup
| Prop | Type | Default | Description |
|---|---|---|---|
label* | React.ReactNode | — | The small-caps sticky label ("Today"), also the list's name. |
Data attributes and CSS variables on InboxGroup
| Attribute | Values |
|---|---|
data-slot | "inbox-group" |
InboxItem
| Prop | Type | Default | Description |
|---|---|---|---|
title* | React.ReactNode | — | The sentence: plain text, or a template with InboxEmphasis for the actor and record. |
actions | InboxItemAction[] | — | Up to three action chips. |
avatar | { name: string; src?: string; } | — | The actor, as a 28px avatar: { name, src? }. Takes precedence over icon. |
count | number | — | How many events this row stands for; above 1 shows a count. |
destructive | boolean | false | Draw the icon in the destructive ink (a failure). |
href | string | — | Where the row goes; the whole row is the link. |
icon | React.ReactNode | — | A system event's icon, drawn muted in a 28px tile. |
linkRender | React.ReactElement<Record<string, unknown>, string | React.JSXElementConstructor<any>> | — | The link element to render instead of <a> (a router Link); it receives href, className
and children. |
menu | React.ReactNode | — | DropdownMenuItems for the row's ⋯ menu; omit to hide it. |
meta | React.ReactNode | — | The muted line under the title ("Skyline Tower B · Project"). |
onToggleRead | (() => void) | — | Called by the hover Mark read / Mark unread toggle; omit to hide it. |
time | string | number | Date | — | When it happened; shown short, absolute on hover. |
unread | boolean | false | Unread: a soft full-bleed tint, the title in the foreground colour (read titles are muted) and an sr-only "Unread". |
Data attributes and CSS variables on InboxItem
| Attribute | Values |
|---|---|
data-icon | "inline-start" |
data-slot | "inbox-item" |
data-unread | mirrors a prop or state value |
InboxEmpty
| Prop | Type | Default | Description |
|---|---|---|---|
description | React.ReactNode | — | |
icon | React.ReactNode | a BellOff icon | |
title | React.ReactNode | "You're all caught up" |
InboxError
| Prop | Type | Default | Description |
|---|---|---|---|
onRetry* | () => void | — | Called by "Try again". |
description | React.ReactNode | — | |
retrying | boolean | false | |
title | React.ReactNode | "Couldn't load notifications" |
InboxSkeleton
| Prop | Type | Default | Description |
|---|---|---|---|
label | string | "Loading notifications" | |
rows | number | 5 |
Accessibility
- Each group is a list named by its label; the whole row is one link (
href, or your router link throughlinkRender), and the chips and hover controls sit above it. - The hover controls are always in the tab order and show on
focus-within; on touch screens they are always visible. - An unread row carries a soft tint and an sr-only "Unread:" before its title.
- The header icon buttons are named by their tooltips.
| Contract | States tested |
|---|---|
| Behaviour | default, unread, loading, empty, error |
| Accessibility | labeled |
| Visual | default, error, dot-intent-default, dot-intent-destructive |