Interactive

Notification Badge

A count badge that springs onto any element and disappears the moment the count reaches zero.

A badge that anchors itself to the top-right of whatever you put in the default slot, and reacts to the count changing. It is a thin wrapper: the badge itself is a Nuxt UI UBadge. The badge scales and fades in with AnimatePresence when the count rises above zero, and on every change the previous number slides out vertically while the new one slides in.

Two rules come straight from the source. count <= 0 hides the badge entirely — there is no "zero" state to render — and count > max is clamped to a max+ label so an unread inbox never grows past a fixed width.

The whole wrapper is a UTooltip, so anything you pass as tooltip becomes the trigger's description on hover and on keyboard focus.

Usage

Wrap any element — an icon button, an avatar, a nav link — and feed it count. The badge is positioned absolutely, so it never reflows the element underneath.

3
Inbox
3
<template>
  <div class="flex w-full max-w-md flex-col gap-4">
    <div class="flex items-center justify-between rounded-2xl border p-4">
      <div class="flex items-center gap-3">
        <MNotificationBadge
          :count="count"
          :tooltip="tooltip"
        >
          <UButton
            icon="i-lucide-bell"
            color="neutral"
            variant="outline"
            aria-label="Notifications"
          />
        </MNotificationBadge>
        <span class="text-sm font-medium">
          Inbox
        </span>
      </div>
      <span class="text-xs text-muted tabular-nums">
        {{ badgeLabel }}
      </span>
    </div>

    <div class="flex flex-wrap items-center gap-2">
      <UButton
        label="New message"
        icon="i-lucide-plus"
        size="sm"
        @click="count++"
      />
      <UButton
        label="Overflow"
        icon="i-lucide-chevrons-up"
        size="sm"
        variant="outline"
        color="neutral"
        @click="count = 150"
      />
      <UButton
        label="Mark all read"
        icon="i-lucide-check-check"
        size="sm"
        variant="outline"
        color="neutral"
        @click="count = 0"
      />
    </div>
  </div>
</template>

<script setup lang="ts">
const count = ref(3)

const badgeLabel = computed(() => (count.value > 99 ? '99+' : String(count.value)))

const tooltip = computed(() => {
  if (count.value === 0)
    return 'No unread messages'
  return `${count.value} unread ${count.value === 1 ? 'message' : 'messages'}`
})
</script>

Customize

3

Customize

Count3
Tooltip3 unread messages
Max99
Colorerror
Badge Sizesm

Props

Prop Type Default Description
countnumber-The number the badge represents. Required. At 0 or below the badge is not rendered at all, and the enter/exit animation does not play.
tooltipstring''Text for the UTooltip that wraps the whole thing, so hovering or focusing the anchored element describes the count in words. Defaults to empty, which leaves the trigger with nothing extra announced.
maxnumber99Upper bound for the label. Anything above it renders as `${max}+`. It only affects the text — the badge is still rendered at its full width.
colorstring'error'Forwarded straight to UBadge's color, so any Nuxt UI colour alias works (error, success, info, primary, or a raw hex value).
badgeSize'xs' | 'sm' | 'md' | 'lg' | 'xl''sm'Forwarded to UBadge's size. The wrapper's own padding and min-w-[1.25rem] are fixed regardless, so larger sizes grow the badge but not the offset from the corner.

Slots

Slot Props Description
default-The element the badge anchors to. Rendered inside a relative inline-flex wrapper, so give it whatever size you like — the badge positions itself against that wrapper.

UI

This component's theme exposes the slots below. Override any of them per instance with the ui prop, or app-wide under ui.notificationBadge in app.config.ts.

Prop Type Default Description
uiobject-Per-slot classes merged over the theme defaults.
Slot Props Description
root-relative inline-flex
badge-absolute -top-2 -right-2 z-10
badgeContent-rounded-full px-1.5 min-w-[1.25rem] h-5 flex items-center justify-center

Accessibility

The badge is plain text inside the wrapper, with no role and no live region of its own. Because the wrapper is also the tooltip's trigger, that text becomes part of the trigger's accessible name — a button containing this component reads as something like "3 Notifications", and a change from 3 to 4 is silent to a screen reader. Announce the change yourself if it matters.

No reduced-motion branch exists here. The badge is animated by a fixed spring (stiffness: 500, damping: 20), and that spring runs whatever the user's preference is. It is short and small, but it is not opt-out.

Performance

  • The badge subtree exists only while count > 0; AnimatePresence keeps it mounted just long enough to play the exit.
  • The animation is transform-only (scale on show/hide and a small y slide per count change) plus opacity, and the badge is position: absolute, so neither entering nor updating reflows the element underneath.
  • No timers, no listeners, no per-frame work. The only reactive computation is the displayText clamp.
color is typed as a plain string in the component's own props, so a typo is not caught by the type checker — it fails silently and the badge falls back to UBadge's default colour. If the badge is the wrong colour, check the string before anything else.
Copyright © 2026