Content
Badge
Count what's new, or label a row with a short status.
import { Avatar } from "macbetter/avatar"
import { BadgeAnchor, NotificationBadge } from "macbetter/badge"
import { Button } from "macbetter/button"
import { BellIcon, InboxIcon } from "lucide-react"
export default function Example() {
return (
<div style={{ display: "flex", alignItems: "center", gap: 32 }}>
<BadgeAnchor>
<Avatar shape="app" size="xl" name="Mail" color="blue" />
<NotificationBadge size="lg" count={12} label="unread messages" />
</BadgeAnchor>
<BadgeAnchor>
<Avatar shape="app" size="xl" name="App Store" color="cyan" />
<NotificationBadge size="lg" count={240} label="updates" />
</BadgeAnchor>
<BadgeAnchor>
<Button variant="plain" tone="neutral" shape="circle" aria-label="Inbox">
<InboxIcon aria-hidden="true" />
</Button>
<NotificationBadge count={3} label="new" />
</BadgeAnchor>
<BadgeAnchor>
<Button variant="plain" tone="neutral" shape="circle" aria-label="Notifications">
<BellIcon aria-hidden="true" />
</Button>
<NotificationBadge label="New notifications" />
</BadgeAnchor>
</div>
)
}When to use it
Use a NotificationBadge to say how many new things are waiting behind an icon: unread messages, available updates, new notifications. It's always red with white digits, as on app icons, tab bars and the Dock, because people already know what it means. Show a count only for things that need attention; a badge that's always there stops being noticed. With no count, it's a plain dot that says only "something new".
Use a Badge to label a row, a card or a title with a word or two: a plan, a state, "New" or "Beta". It isn't a button. If pressing it does something, use a button.
Usage
import { Badge, BadgeAnchor, NotificationBadge } from "macbetter/badge"Notification counts
Wrap the icon and the badge in a BadgeAnchor to pin the badge to its top trailing corner. Counts above max (99 by default) read as "99+". A count of zero renders nothing, so you can pass the number straight through. Pass label to say what is being counted.
Labels
tinted is the quiet default. Use filled for the one label in a view that should stand out, and outline beside other filled content. The text color is adjusted for each hue so it holds 4.5:1 in light and dark.
import { Badge } from "macbetter/badge"
const colors = ["accent", "red", "yellow", "green", "teal", "blue", "indigo", "purple", "gray"] as const
export default function Example() {
return (
<div style={{ display: "grid", gap: 12, justifyItems: "start" }}>
{(["tinted", "filled", "outline"] as const).map((variant) => (
<div key={variant} style={{ display: "flex", flexWrap: "wrap", gap: 8 }}>
{colors.map((color) => (
<Badge key={color} variant={variant} color={color}>
{color === "accent" ? "New" : color[0]?.toUpperCase() + color.slice(1)}
</Badge>
))}
</div>
))}
</div>
)
}Sizes and icons
import { Badge } from "macbetter/badge"
import { SparklesIcon } from "lucide-react"
export default function Example() {
return (
<div style={{ display: "flex", alignItems: "center", gap: 10 }}>
<Badge size="sm">Beta</Badge>
<Badge>Beta</Badge>
<Badge variant="filled">
<SparklesIcon aria-hidden="true" />
Pro
</Badge>
<Badge variant="filled" color="green">
Paid
</Badge>
</div>
)
}Status dot
The dot variant puts a colored dot beside plain text, for a status in a table or a list where a filled capsule in every row would be loud.
import { Badge } from "macbetter/badge"
const services = [
{ name: "API", status: "Operational", color: "green" },
{ name: "Dashboards", status: "Degraded", color: "yellow" },
{ name: "Webhooks", status: "Outage", color: "red" },
{ name: "Exports", status: "Paused", color: "gray" },
] as const
export default function Example() {
return (
<div style={{ display: "grid", gridTemplateColumns: "auto auto", gap: "10px 32px", alignItems: "center" }}>
{services.map((service) => (
<div key={service.name} style={{ display: "contents" }}>
<span>{service.name}</span>
<Badge variant="dot" color={service.color}>
{service.status}
</Badge>
</div>
))}
</div>
)
}Platform behavior
Labels are 18 points tall with 11-point semibold text on macOS, and 22 points with 13-point text on iOS and iPadOS, so they sit level with each platform's body text. Notification badges are the same on every platform: sm for a toolbar icon, md for a tab bar or a sidebar row, lg for an app icon.
Accessibility
- The visible digits are hidden from screen readers; a hidden sentence reads instead: "12 unread messages", or "More than 99 updates".
- A dot with no count reads only its
label, so give it one, or leave it out when the control's own name already says there's something new. - A badge's meaning never rests on color alone: the dot variant and colored labels always carry text.
API
Badge
| Prop | Type | Default | Description |
|---|---|---|---|
variant | "tinted" | "filled" | "outline" | "dot" | "tinted" | How the color is applied. |
color | SystemColor | "accent" | "gray" | "accent" | Any system color, the theme's accent, or gray. |
size | "sm" | "md" | "md" | `sm` fits inside dense table rows. |
NotificationBadge
| Prop | Type | Default | Description |
|---|---|---|---|
count | number | — | Leave it out for a dot. Zero renders nothing. |
max | number | 99 | Counts above this read as `max+`. |
label | string | — | What is being counted, read after the number: "unread messages". |
size | "sm" | "md" | "lg" | "md" | 14, 18 or 24 pixels tall. |
BadgeAnchor
A span that pins a NotificationBadge inside it to the top trailing corner of its other content.
Data attributes and variables
| Attribute or variable | On | Description |
|---|---|---|
data-variant, data-color, data-size | .mb-badge | The props. |
data-dot | .mb-notification-badge | Present when there's no count. |
--mb-badge-color | .mb-badge | The hue, for a color outside the palette. |
--mb-badge-fill | .mb-badge | A filled badge's background. Light hues keep their color and take dark text; the rest deepen just enough for white text to hold 4.5:1. |
--mb-notification-badge-color | .mb-notification-badge | The red. |