Browse docs
Browse docs
Show an unread count. Flag a status. Indicate new content.
import { Badge } from '@dashforge/tw';
import { Bell } from 'lucide-react';
<Badge content={3} color="danger">
<Bell />
</Badge><Badge content={12}><Bell /></Badge>
<Badge dot color="success"><Avatar src="/u/jane.png" /></Badge>
<Badge content={150} max={99} color="primary"><Inbox /></Badge>
<Badge invisible={count === 0} content={count}><Cart /></Badge>Two modes — number content with max overflow, or boolean dot indicator:
import { Badge, IconButton } from '@dashforge/tw';
import { Bell } from 'lucide-react';
<Badge content={3} color="danger">
<IconButton aria-label="Notifications (3 unread)"><Bell /></IconButton>
</Badge>
<Badge content={150} max={99} color="warning">
<IconButton aria-label="Notifications (150 unread)"><Bell /></IconButton>
</Badge>
<Badge dot color="success">
<IconButton aria-label="Online"><Bell /></IconButton>
</Badge>When content exceeds max, the badge renders {max}+ instead. Defaults: max={99}.
<Badge content={150} max={99} color="primary"><Bell /></Badge>
{/* renders "99+" */}
<Badge content={9999} max={999}><Bell /></Badge>
{/* renders "999+" */}By default, content === 0 hides the badge. Pass showZero to keep it visible:
<Badge content={0} showZero color="info"><Bell /></Badge>
{/* renders "0" */}Toggle visibility imperatively without unmounting:
<Badge content={count} invisible={count === 0}>
<Cart />
</Badge>This is different from showZero — invisible accepts an arbitrary boolean (e.g. tied to a feature flag or visibility toggle).
import { Avatar, Badge, Stack } from '@dashforge/tw';
<Stack direction="row" gap={4}>
<Badge content={3} placement="top-right"><Avatar name="A" size="lg" /></Badge>
<Badge content={3} placement="top-left"><Avatar name="A" size="lg" /></Badge>
<Badge content={3} placement="bottom-right"><Avatar name="A" size="lg" /></Badge>
<Badge content={3} placement="bottom-left"><Avatar name="A" size="lg" /></Badge>
</Stack>The overlap prop tunes badge positioning to the anchor's shape. Default is rectangular (suits square/rectangular anchors like buttons, icons, cards). Use circular when the anchor is a circular shape (most importantly <Avatar>) — applies a 14% inset so the badge sits naturally on the circle edge.
import { Avatar, Badge, IconButton } from '@dashforge/tw';
import { Bell } from 'lucide-react';
{/* rectangular — for square anchors like buttons */}
<Badge content={3} overlap="rectangular">
<IconButton aria-label="Notifications"><Bell /></IconButton>
</Badge>
{/* circular — adds 14% inset, tuned for Avatar */}
<Badge content={3} overlap="circular">
<Avatar name="A" size="lg" />
</Badge>Default withRing is true — adds ring-neutral-50 around the badge for separation from the anchor (useful when the badge sits on a similarly-colored background, e.g. an Avatar with primary tone).
<Badge content={3} color="danger" withRing>
<Avatar src="/u/jane.png" />
</Badge>Disable for flush badges:
<Badge content={3} withRing={false}><Bell /></Badge>{/* Hide the unread badge from users without inbox access */}
<Badge content={unreadCount}
access={{ requires: 'inbox.view', when: 'denied:hide' }}
>
<Inbox />
</Badge>
{/* Show only when feature flag is on */}
<Badge content={pendingApprovals}
visibleWhen={(engine) => engine.getValue('flags.approvals') === true}
>
<Inbox />
</Badge><Badge> is wrapper-only — it expects a child to anchor to. For an inline standalone pill (e.g. a tag in a Table cell), use <Chip> instead.
Configure <Badge> defaults application-wide.
import { patchTheme } from '@dashforge/tw-theme';
patchTheme({
components: {
Badge: {
defaults: { color: 'danger', placement: 'top-right', overlap: 'rectangular' },
},
},
});Configurable axes (BadgeVariantProps):
| Axis | Type | Notes |
|---|---|---|
color | intent role | Semantic color for the badge fill. |
placement | 'top-right' | 'top-left' | 'bottom-right' | 'bottom-left' | Corner anchor. |
overlap | 'rectangular' | 'circular' | Anchor shape offset. |
withRing | boolean | Contrast ring around the badge (visually pops off surface). |
Non-visual axes (content, dot, max, showZero, invisible, access, visibleWhen) are not theme-configurable — state and data-driven per-instance.
Precedence chain (lowest → highest):
theme.components.Badge.defaults (application-wide).<Badge color="primary" placement="bottom-right" />) — wins over theme.sx — appended to the badge indicator and wins over conflicting classes via tailwind-merge.TypeScript: theme.components.Badge.defaults autocompletes to the four axes above.
Reactivity: useComponentDefaults('Badge') subscribes to the theme store — patchTheme re-renders every mounted instance inheriting the changed axis.
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | — | The anchor element (Avatar / IconButton / button / Card / custom). |
access | AccessRequirement | — | RBAC requirement — hides the badge for unauthorized subjects. The wrapped anchor element remains visible (use Box access to gate the anchor too).Use case: admin-only notification count, role-gated indicators. |
className | string | — | Standard className — appended to the root via cn(). |
color | BadgeColor | 'danger' | Semantic intent for the badge surface. Drives bg + text color pair. Default 'danger' mirrors the typical "needs attention" notification bubble. |
content | number | string | — | Numeric or string content to display inside the badge. - number → formatted via max (overflow renders as "N+") - string → rendered verbatim (e.g., "NEW") - omitted + dot=false → no badge rendered (or only at zero if showZero=true) |
dot | boolean | — | Dot-only mode — small circle without content. When true, content and max are ignored. |
invisible | boolean | false | Imperative hide — keeps the badge in the DOM but applies display: none. Use for opacity/scale fade animations without unmounting. |
max | number | 99 | Overflow threshold for numeric content. Values above render as {max}+. Set to -1 to disable overflow (always raw number). |
overlap | BadgeOverlap | 'rectangular' | Anchor shape — controls badge offset. - 'rectangular' (default) sits at the bounding-box corner - 'circular' offsets to align with the circular anchor edge (use with <Avatar shape="circle">) |
placement | BadgePlacement | 'top-right' | Corner of the anchor the badge attaches to. |
showZero | boolean | false | Whether to render the badge when numeric content is 0. Hidden by default — the typical notification UX is "no badge = nothing to see". |
slotProps | BadgeSlotProps | — | Per-slot overrides — root + badge. |
sx | ClassValue | — | Root-element class shortcut. |
visibleWhen | (engine: Engine) => boolean | — | Reactive visibility predicate. When false, the badge dot/count is not rendered (children always render). Use for state-driven indicators tied to engine values. |
withRing | boolean | true | White/neutral ring around the badge — visually separates from the anchor surface. Token-driven via ring-neutral-50 which auto-inverts via the dashforgePreset CSS-var swap (no dark: needed). |
<Badge> is a small compound component with 2 named slots. Each slot accepts a { className?: string } override via slotProps:
| Slot | Purpose |
|---|---|
root | The <span> wrapping the anchor (children) that provides the positioning context. |
badge | The floating badge pill / dot itself, positioned at the specified placement. |
<Badge
content={7}
slotProps={{
root: { className: 'inline-block' },
badge: { className: 'text-[10px] font-mono' },
}}
>
<IconButton aria-label="Notifications"><BellIcon /></IconButton>
</Badge>Use sx for a class override on the badge indicator itself (shortcut for slotProps.badge.className); use slotProps when you also need to reach the positioning wrapper.
aria-hidden="true" — it's purely decorative on top of the anchor.aria-label. Example:<Badge content={unread} color="danger">
<IconButton aria-label={`Notifications (${unread} unread)`}>
<Bell />
</IconButton>
</Badge>Screen readers will announce "Notifications (3 unread), button" — the badge is just visual emphasis.
ring-neutral-50 instead of ring-white — the neutral palette auto-inverts via CSS var swap, so ring-neutral-50 works in both light and dark mode without dark: override. See Design decisions.overlap="circular" exists for Avatar: rectangular anchors look fine with default overlap, but Avatars need a slight inset (14%) so the badge sits on the circle edge — that's what the circular value does.<Chip>, not Badge. Badge requires a child to anchor to.