docs / core / notification-badge
NotificationBadge
a small count or dot pinned to the corner of whatever it wraps: an icon, a button, an avatar. it vanishes at zero, so silence stays silent.
intensity · recedesrsc · server
provenance
shaped from sisu-pluslayer
core / primitivesintensity
recedes · safe anywhere, including dense surfacescomposition
wraps an icon-only Button, an avatar, a nav itemdot mode for presence, count mode for unreadnot a status Badge. it counts, it does not labelnever on a bare icon with no accessible name of its owna11y
the dot isaria-hidden · the wrapper never steals the child's role or namejest-axelive demo · try it out
3
customize
countthe number shown
maxcap before N+
dotbare presence dot, no number
toneindicator colour
showZerostay visible at 0
usage
import { Button } from "usva/primitives/button";
import { NotificationBadge } from "usva/primitives/notification-badge";
<NotificationBadge count={3}>
<Button variant="outline" iconOnly aria-label="Notifications">
<BellIcon />
</Button>
</NotificationBadge>props
| prop | type | default | notes |
|---|---|---|---|
| count | number | 0 | the count to show. hidden at 0 unless showZero. |
| max | number | 9 | cap before showing "N+". |
| dot | boolean | false | a bare presence dot instead of a number. |
| tone | "accent" | "accent-alt" | "danger" | "warning" | "danger" | indicator color. |
| showZero | boolean | false | keep the badge visible at 0. |
get it
npx shadcn add https://usva.build/r/notification-badge.jsonsource · components/ui/notification-badge.tsxexactly what this command copies
import * as React from "react";
import { cn } from "@/lib/utils";
export type NotificationTone = "accent" | "accent-alt" | "danger" | "warning";
export interface NotificationBadgeProps
extends React.HTMLAttributes<HTMLSpanElement> {
/** The count to show. Hidden at 0 unless `showZero`. */
count?: number;
/** Cap before showing "N+". Defaults to 9. */
max?: number;
tone?: NotificationTone;
/** Show a bare dot instead of a number. */
dot?: boolean;
showZero?: boolean;
}
const toneClass: Record<NotificationTone, string> = {
accent: "bg-accent text-on-accent",
"accent-alt": "bg-accent-alt text-on-accent",
danger: "bg-danger text-on-accent",
warning: "bg-warning text-on-accent",
};
export const NotificationBadge = React.forwardRef<
HTMLSpanElement,
NotificationBadgeProps
>(
(
{
className,
count = 0,
max = 9,
tone = "danger",
dot = false,
showZero = false,
children,
...props
},
ref,
) => {
const visible = dot || count > 0 || showZero;
return (
<span
ref={ref}
className={cn("relative inline-flex", className)}
{...props}
>
{children}
{visible &&
(dot ? (
<span
aria-hidden="true"
className={cn(
"absolute -top-0.5 -right-0.5 h-2.5 w-2.5 rounded-full ring-2 ring-surface",
toneClass[tone],
)}
/>
) : (
<span
className={cn(
"absolute -top-1.5 -right-1.5 inline-flex h-[1.15rem] min-w-[1.15rem] items-center justify-center rounded-full px-1 font-mono text-[0.625rem] font-semibold leading-none tabular-nums ring-2 ring-surface",
toneClass[tone],
)}
>
{count > max ? `${max}+` : count}
</span>
))}
</span>
);
},
);
NotificationBadge.displayName = "NotificationBadge";