Skip to content
Component installs need the registry setup
VegaStack Design

Notification Bell

A bell icon button with an unread-count badge overlay — presentational; the app supplies the count.

Status
stable
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-bell

Install with the pinned shadcn CLI. Bare shadcn does not verify VegaStack signature or integrity.

pnpm dlx shadcn@4.21.0 add @vegastack/notification-bell

Run 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):

NotificationBell — data-slot="notification-bell" | "notification-bell-badge"
NotificationDot — data-slot="notification-dot"

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.

PropTypeDefaultDescription
aria-labelstring'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").
countnumber0Unread 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.
dotbooleanfalseRender a minimal dot instead of the numeric count when there are unread items — useful in dense chrome where the exact number is noise.
loadingboolean—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

AttributeValues
data-slot"notification-bell" | "notification-bell-badge"
data-unread""

NotificationDot

PropTypeDefaultDescription
classNamestring—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

AttributeValues
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-hidden and pointer-events-none, so it never double-announces or intercepts the click.
  • NotificationDot is 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's Button, size="icon"); keyboard Enter / Space activate it, and :focus-visible shows the neutral 2px outline ring (outline-ring, the global focus style) — the bell adds no focus ring of its own.
KeyAction
EnterActivate the bell
SpaceActivate the bell
TabMove focus to / from the bell
ContractStates tested
Behaviourdefault, error, dot, count-label
Accessibilitylabeled
Visualdefault, error, dot-intent-default, dot-intent-destructive

Do / Don't

Do
Pass the unread count from your app's own data and let the component announce it.
Don't
Fetch inside the bell or hand-write the count into aria-label — it's appended for you.
Do
Use NotificationDot for every unread marker, so a row, a nav item and the bell show the same dot.
Don't
Draw a one-off coloured circle, or rely on the dot alone to tell a screen-reader user a row is unread.

On this page