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.
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | (required) | The anchor element. The badge overlays this. |
content | ReactNode | number | — | The badge content. Numbers get max-overflow treatment. |
dot | boolean | false | Render as dot (no content). Use for boolean status indicators. |
max | number | 99 | When content is a number greater than this, render {max}+. |
showZero | boolean | false | Render even when content === 0. |
invisible | boolean | false | Force hide without unmounting. |
color | 'primary' | 'secondary' | 'success' | 'warning' | 'danger' | 'info' | 'neutral' | 'danger' | Semantic intent for badge bg + fg. |
placement | 'top-right' | 'top-left' | 'bottom-right' | 'bottom-left' | 'top-right' | Corner placement. |
overlap | 'rectangular' | 'circular' | 'rectangular' | Positioning fine-tune. circular adds 14% inset (Avatar-tuned). |
withRing | boolean | true | Show ring-neutral-50 separator. |
access | AccessSpec | — | RBAC gating. |
visibleWhen | (engine: Engine) => boolean | — | Engine-reactive predicate. |
className | string | — | Merged via tailwind-merge. |
sx | string | — | String-form Tailwind override. |
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.