Notification Bell
A bell icon button with an unread-count badge overlay — presentational; the app supplies the count.
- Status
- Since
0.1.0- Accessibility pattern
- native button with counted name
Last updated
Install
Configure registry credentials first using the Quickstart. Before copying this item or any transitive registry dependency, verify the signed manifest and save the exact bytes for Notification Bell before installation. Retain the digest printed by this command.
pnpm exec vegastack-design verify notification-bellInstall with the pinned shadcn CLI. Bare shadcn does not verify VegaStack signature or integrity.
pnpm dlx shadcn@4.21.0 add @vegastack/notification-bellRun the exact offline --post-write command printed by the preflight, using its saved item path and independently retained --expected-integrity digest. Use the saved preflight for every transitive registry dependency too; stop on any mismatch before using the components.
The same command installs the registry items it composes: @vegastack/badge, @vegastack/button, @vegastack/use-animation-replay.
Usage
import { NotificationBell } from "@/components/ui/notification-bell";
<NotificationBell count={unread} onClick={openPanel} />;NotificationBell is presentational — it never fetches. Your application owns the unread count
(from its own query, websocket, etc.) and the onClick that opens the notifications surface. The
component just renders a bell Button at an icon size with the count
badge overlaid at the top inline-end edge.
Anatomy
Notification Bell exports two parts: the bell and the shared unread dot. Every exported part, with
the data-slot it renders (generated from the canonical source):
Examples
Counts and indicators
The badge appears only when count > 0, shows the number (capped to 99+), or a minimal dot when
dot is set. The count is a solid destructive pill and the dot a solid info-blue circle, both anchored inside the button's top inline-end corner with a background ring, so they stay in bounds when pressed. count={0} renders the bare bell with no badge.
Shared unread dot
NotificationDot is the one unread dot: the 8px solid circle the bell draws in dot mode, exported
for unread rows in a list, an inbox or a nav item. Its default intent is the solid info blue, for
"unread"; intent="destructive" is for something that needs attention. The dot is aria-hidden, so
the row it marks must say "unread" in its own accessible name.
- Design review for the billing page, unread
- Quarterly planning notes
- Invoice 1042 failed to send, unread
Count label
countLabel words the count in the bell's accessible name, after the base aria-label. It
receives the real count; the default reads "3 unread" and caps at "99+ unread" like the badge.
Overflow boundary
count={99} is the exact cap and still renders the number; count={100} is the first value that
overflows to the 99+ label.
Motion
Static unread state stays still when the page mounts. After mount, the badge pops in
(motion-pop-in) when unread activity first appears; in count mode it replays when the displayed
number changes.
Sizes & variants
NotificationBell extends Button, so every Button prop — size
(icon-xs / icon-sm / icon / icon-lg), variant, disabled, loading, render, onClick —
forwards straight through to the underlying button. Two are managed for you: children (the bell
icon) and aria-label (the count-folded accessible name). className is the one exception to
"straight through" — it lands on the wrapper <span> that positions the badge, not on the button,
so reach for it to place the bell and for render to restyle it.
Playground
Try the count values — including the 99+ overflow — and the dot mode, then copy the generated JSX.
<NotificationBell />API Reference
NotificationBell adds the four props below and inherits the rest from
Button (e.g. size, variant, disabled, loading,
render, onClick), minus children / aria-label, which it manages.
| Prop | Type | Default | Description |
|---|---|---|---|
aria-label | string | 'Notifications' | Accessible name for the trigger. The unread count is appended to the
announced name automatically, so pass the base label only (e.g.
"Notifications"). |
count | number | 0 | Unread notification count, supplied by the app. 0 (or omitted) hides the
badge; values above 99 render as "99+". |
countLabel | ((n: number) => string) | (n) => `${n} unread` | Words for the unread count in the accessible name, after the base label: "Notifications, 3 unread". Receives the real count; the default caps it at "99+" like the badge. |
dot | boolean | false | Render a minimal dot instead of the numeric count when there are unread items — useful in dense chrome where the exact number is noise. |
loading | boolean | — | Shows a spinner over the label, blocks activation and sets aria-busy. The label keeps its
box at opacity: 0, so the button's width does not move and its accessible name survives
(API-5, A11Y-12). |
size | "default" | "icon" | "icon-lg" | "icon-sm" | "icon-xs" | "lg" | "sm" | "xs" | null | — | |
variant | "default" | "destructive" | "ghost" | "link" | "outline" | "secondary" | null | — |
Data attributes and CSS variables on NotificationBell
| Attribute | Values |
|---|---|
data-slot | "notification-bell" | "notification-bell-badge" |
data-unread | "" |
NotificationDot
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | — | Classes merged onto the dot. |
intent | "default" | "destructive" | "default" | The dot's fill. default is the info blue, for "unread" on a row or a
nav item; destructive is for something that needs attention. |
Data attributes and CSS variables on NotificationDot
| Attribute | Values |
|---|---|
data-slot | "notification-dot" |
Accessibility
- The unread count is folded into the accessible name automatically — screen readers hear
"Notifications, 3 unread". Pass the base name via
aria-label(defaults to"Notifications"); do not append the count yourself. - The visual count badge is
aria-hiddenandpointer-events-none, so it never double-announces or intercepts the click. NotificationDotis decorative everywhere it is used. A row or control that shows it carries "unread" in its own accessible name, the way the bell folds its count in.- Renders a native
<button type="button">(upstream'sButton,size="icon"); keyboard Enter / Space activate it, and:focus-visibleshows the neutral 2px outline ring (outline-ring, the global focus style) — the bell adds no focus ring of its own.
| Key | Action |
|---|---|
| Enter | Activate the bell |
| Space | Activate the bell |
| Tab | Move focus to / from the bell |
| Contract | States tested |
|---|---|
| Behaviour | default, error, dot, count-label |
| Accessibility | labeled |
| Visual | default, error, dot-intent-default, dot-intent-destructive |