Skip to content
Component installs need the registry setup— the Base UI shadcn project, the @vegastack namespace and the Cloudflare Access service token.
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

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

The 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 IconButtonPropssize (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.

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").
classNamestring | ((state: ButtonState) => string | undefined)Classes or a Base UI state resolver merged with the button variants.
countnumber0Unread notification count, supplied by the app. 0 (or omitted) hides the badge; values above 99 render as "99+".
data-loadingstringLoading-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-slotstring'button'Slot marker for wrapper components that compose Button through Base UI render and need their own generated registry slot.
dotbooleanfalseRender a minimal dot instead of the numeric count when there are unread items — useful in dense chrome where the exact number is noise.
loadingbooleanfalseShows 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.
shapeIconButtonShape'square'Outline shape. round is the sanctioned way to get a circular control — a rounded-full override on a Button is not.
sizeIconButtonSize'md'Square size, from the one --size-* vocabulary.
toneButtonTone
variant"cta" | "ghost" | "link" | "outline" | "soft" | "solid"

Data attributes and CSS variables on NotificationBell

AttributeValues
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-hidden and pointer-events-none, so it never double-announces or intercepts the click.
  • Renders a native <button type="button"> (via IconButtonButton); keyboard Enter / Space activate it, and :focus-visible shows the neutral 2px outline ring (outline-ring, the global focus style) — the bell uses the default IconButton variant, which carries no per-component focus ring.
KeyAction
EnterActivate the bell
SpaceActivate the bell
TabMove focus to / from the bell
ContractStates tested
Behaviourdefault, error
Accessibilitylabeled
Visualdefault, error

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.

On this page