Skip to content
Component installs need the registry setup
VegaStack Design

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
stable
Since
0.23.4
Accessibility pattern
named lists, row links, hover controls on focus

Last updated

Inbox

Today
Earlier
You’re all caught up

Install

Add Inbox from the VegaStack registry. The CLI verifies the item's integrity hash before writing it.

pnpm dlx shadcn@latest add @vegastack/inbox

The 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.
  • Inbox fills its container. Dock it in a Sheet, a Popover or 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 — data-slot="inbox" | "inbox-body"
InboxHeaderAction
InboxMenuAction
InboxMarkAllRead
InboxFilters
InboxList — data-slot="inbox-list"
InboxGroup — data-slot="inbox-group"
InboxEmphasis
InboxItem — data-slot="inbox-item"
InboxEmpty
InboxError
InboxSkeleton
  • Inbox — the frame: title, header actions (InboxMarkAllRead, InboxMenuAction, any InboxHeaderAction), the ✕ when onClose is set, the filters row, a scrolling body and a footer — or, with onLoadMore, 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. InboxList is 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

Today
Earlier
You’re all caught up

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

Today

Inbox

Yesterday

Empty

Inbox

You’re all caught up

New assignments and meeting updates show up here.

Loading

Inbox

API Reference

PropTypeDefaultDescription
actionsReact.ReactNode—Header icon buttons before the close button — usually InboxHeaderActions ("Mark all read", a settings menu).
closeLabelstring"Close inbox"Accessible name of the close button.
endLabelReact.ReactNode"You’re all caught up"The end of the list once hasMore is false.
filtersReact.ReactNode—The row under the title — usually InboxFilters.
footerReact.ReactNode—Content under the list; with onLoadMore the end of the list is drawn for you.
hasMorebooleanfalseMore rows exist; false ends the list on endLabel.
loadingMorebooleanfalseA page is loading: three skeleton rows show under the list.
loadMoreErrorbooleanfalseThe 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).
titleReact.ReactNode"Inbox"The heading.

Data attributes and CSS variables on Inbox

AttributeValues
data-slot"inbox" | "inbox-body"

InboxFilters

PropTypeDefaultDescription
onValueChange*(value: InboxFilter) => void—Called with the filter a chip selects.
value*InboxFilter—The selected filter.
classNamestring—
unreadCountnumber0The unread count shown on the Unread chip; 0 hides it.

InboxGroup

PropTypeDefaultDescription
label*React.ReactNode—The small-caps sticky label ("Today"), also the list's name.

Data attributes and CSS variables on InboxGroup

AttributeValues
data-slot"inbox-group"

InboxItem

PropTypeDefaultDescription
title*React.ReactNode—The sentence: plain text, or a template with InboxEmphasis for the actor and record.
actionsInboxItemAction[]—Up to three action chips.
avatar{ name: string; src?: string; }—The actor, as a 28px avatar: { name, src? }. Takes precedence over icon.
countnumber—How many events this row stands for; above 1 shows a count.
destructivebooleanfalseDraw the icon in the destructive ink (a failure).
hrefstring—Where the row goes; the whole row is the link.
iconReact.ReactNode—A system event's icon, drawn muted in a 28px tile.
linkRenderReact.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.
menuReact.ReactNode—DropdownMenuItems for the row's ⋯ menu; omit to hide it.
metaReact.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.
timestring | number | Date—When it happened; shown short, absolute on hover.
unreadbooleanfalseUnread: 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

AttributeValues
data-icon"inline-start"
data-slot"inbox-item"
data-unreadmirrors a prop or state value

InboxEmpty

PropTypeDefaultDescription
descriptionReact.ReactNode—
iconReact.ReactNodea BellOff icon
titleReact.ReactNode"You're all caught up"

InboxError

PropTypeDefaultDescription
onRetry*() => void—Called by "Try again".
descriptionReact.ReactNode—
retryingbooleanfalse
titleReact.ReactNode"Couldn't load notifications"

InboxSkeleton

PropTypeDefaultDescription
labelstring"Loading notifications"
rowsnumber5

Accessibility

  • Each group is a list named by its label; the whole row is one link (href, or your router link through linkRender), 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.
ContractStates tested
Behaviourdefault, unread, loading, empty, error
Accessibilitylabeled
Visualdefault, error, dot-intent-default, dot-intent-destructive

Do / Don't

Do
Name the actor and the record in the title with InboxEmphasis, so identical events tell themselves apart.
Don't
Show a row of identical generic titles such as “Task assigned to you”.
Do
Fold repeats of one event on one record into a single row with count.
Don't
Put more than three action chips on a row, or make every chip primary.

On this page