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
Add Notification Bell from the VegaStack registry. The CLI verifies the item's integrity hash before writing it.
pnpm dlx shadcn@latest add @vegastack/notification-bellThe same command installs the registry items it composes: @vegastack/badge, @vegastack/button, @vegastack/icon-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 IconButton with the count badge overlaid
at the top inline-end edge.
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. count={0} renders the bare bell with no 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 IconButton, so every
IconButtonProps — size (xs / sm / md /
lg), variant, disabled, loading, render, onClick — forwards straight through to the
underlying button. Only children, aria-label, and label are managed for you (the bell icon and
the count-folded accessible name).
Playground
Try the count values — including the 99+ overflow — and the dot mode, then copy the generated JSX.
<NotificationBell />API Reference
NotificationBell adds the three props below and inherits the rest from
IconButtonProps (e.g. size, variant, disabled,
loading, render, onClick), minus children / aria-label / 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"). |
className | string | ((state: ButtonState) => string | undefined) | — | Classes or a Base UI state resolver merged with the button variants. |
count | number | 0 | Unread notification count, supplied by the app. 0 (or omitted) hides the
badge; values above 99 render as "99+". |
data-loading | string | — | Loading-state marker for wrapper components that reflect a host-owned pending
state onto a composed Button without its loading visuals (e.g. SplitButton's
chevron half). The Button's own loading prop always wins when set. |
data-slot | string | 'button' | Slot marker for wrapper components that compose Button through Base UI
render and need their own generated registry slot. |
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 | false | Shows a spinner over the label, disables interaction, and sets aria-busy. The
label keeps its box at opacity: 0, so the button's width does not move across
the flip and its accessible name survives. |
shape | IconButtonShape | 'square' | Outline shape. round is the sanctioned way to get a circular control — a rounded-full
override on a Button is not. |
size | IconButtonSize | 'md' | Square size, from the one --size-* vocabulary. |
tone | ButtonTone | — | |
variant | "cta" | "ghost" | "link" | "outline" | "soft" | "solid" | — |
Data attributes and CSS variables on NotificationBell
| Attribute | Values |
|---|---|
data-slot | "notification-bell" | "notification-bell-badge" |
data-unread | "" |
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. - Renders a native
<button type="button">(viaIconButton→Button); keyboard Enter / Space activate it, and:focus-visibleshows the neutral 2px outline ring (outline-ring, the global focus style) — the bell uses the defaultIconButtonvariant, which carries no per-component focus ring.
| Key | Action |
|---|---|
| Enter | Activate the bell |
| Space | Activate the bell |
| Tab | Move focus to / from the bell |
| Contract | States tested |
|---|---|
| Behaviour | default, error |
| Accessibility | labeled |
| Visual | default, error |