Badge
Free · MITReact Native · ExpoA count, dot or status badge that springs in when it appears, pops when its value changes, caps at 99+ and shrinks away at zero; BadgeAnchor pins it to an icon or avatar.
Installation
$ npx shadcn@latest add https://goodui.dev/native/r/badge.jsonThen run npx expo install --fix so react-native-reanimated match your Expo SDK. Files land in components/goodui-native/badge.
Usage
import { View } from "react-native";
import { Text } from "@/components/goodui-native/foundation";
import { Badge, BadgeAnchor } from "@/components/goodui-native/badge/badge";
export function InboxRow({ unread, avatar }: { unread: number; avatar: React.ReactNode }) {
return (
<View style={{ flexDirection: "row", alignItems: "center", gap: 12 }}>
<BadgeAnchor dot pulse tone="success" overlap="circular" placement="bottom-right" accessibilityLabel="Online">
{avatar}
</BadgeAnchor>
<Text style={{ flex: 1 }}>Design team</Text>
<Badge count={unread} />
</View>
);
}When to use
- Unread or pending counts on a tab, an icon or a list row.
- A dot that says something is new without a number.
- Short status next to a title: New, Live, Pro, Beta, Overdue.
- An online or live indicator on an avatar (dot with pulse).
- Selectable or removable tags: use chip.
- Numbers that are the content itself, such as a balance or a price: use rolling-number.
- Messages the person must read: use toast or inline text.
- Counts on an icon-only button: pass `badge` to icon-button, which also puts the count in the button's label.
Examples
Counts, caps and zero
<Badge count={3} />
<Badge count={128} /> // "99+"
<Badge count={1200} max={999} /> // "999+"
<Badge count={0} /> // hidden; shrinks away when it reaches 0
<Badge count={0} showZero tone="neutral" variant="soft" />Status labels
<Badge label="New" tone="accent" variant="soft" />
<Badge label="Live" dot pulse tone="danger" variant="soft" />
<Badge label="Overdue" tone="warning" variant="outline" size="sm" />On an icon
<BadgeAnchor count={unread} ring="background">
<BellIcon />
</BadgeAnchor>API reference
Badge
| Prop | Type | Default |
|---|---|---|
| count A count. Hidden at 0 unless `showZero`; above `max` it reads "99+". | number | — |
| max Highest count shown as a number. Default 99. | number | 99 |
| showZero Keep the badge visible with a count of 0. | boolean | false |
| label Status text such as "New", "Live" or "Pro". Takes the place of the count. | string | — |
| dot A dot instead of the number (shown while `count` is above 0, or always without a count). With `label`, a leading status dot. | boolean | false |
| pulse The dot breathes outward, for live or online states. Off under reduced motion. | boolean | false |
| variant `solid` for counts that need attention, `soft` and `outline` for quiet status. | "solid" | "soft" | "outline" | "solid" |
| tone `danger` for unread counts; `accent`, `neutral` or a status for everything else. | "success" | "warning" | "danger" | "accent" | "neutral" | "danger" |
| size `sm` is 16 pt tall (dot 8), `md` 20 pt (dot 10). | "sm" | "md" | "md" |
| ring Draws a 2 pt ring in this surface color, so the badge reads as cut out of what it sits on. | BadgeRing | false | false |
| decorative Hide the badge from screen readers because the element it belongs to already says the count in its label. | boolean | false |
| accessibilityLabel Read by screen readers. Defaults to the label, the count ("More than 99") or "New". | string | — |
| labels Overrides for the built-in strings; a GooduiLocaleProvider pack sets them app-wide. | Partial<BadgeLabels> | — |
| style Style for the outer container. | StyleProp<ViewStyle> | — |
| testID Identifier for end-to-end tests. | string | — |
BadgeAnchor
| Prop | Type | Default |
|---|---|---|
| accessibilityLabel Read by screen readers. Defaults to the label, the count ("More than 99") or "New". | string | — |
| testID Identifier for end-to-end tests. | string | — |
| count A count. Hidden at 0 unless `showZero`; above `max` it reads "99+". | number | — |
| max Highest count shown as a number. Default 99. | number | — |
| showZero Keep the badge visible with a count of 0. | boolean | — |
| label Status text such as "New", "Live" or "Pro". Takes the place of the count. | string | — |
| dot A dot instead of the number (shown while `count` is above 0, or always without a count). With `label`, a leading status dot. | boolean | — |
| pulse The dot breathes outward, for live or online states. Off under reduced motion. | boolean | — |
| variant `solid` for counts that need attention, `soft` and `outline` for quiet status. | "solid" | "soft" | "outline" | — |
| tone `danger` for unread counts; `accent`, `neutral` or a status for everything else. | "success" | "warning" | "danger" | "accent" | "neutral" | — |
| size `sm` is 16 pt tall (dot 8), `md` 20 pt (dot 10). | "sm" | "md" | "sm" |
| ring Draws a 2 pt ring in this surface color, so the badge reads as cut out of what it sits on. | BadgeRing | false | "surface" |
| decorative Hide the badge from screen readers because the element it belongs to already says the count in its label. | boolean | — |
| labels Overrides for the built-in strings; a GooduiLocaleProvider pack sets them app-wide. | Partial<BadgeLabels> | — |
| children* The icon, avatar or button the badge belongs to. | ReactNode | — |
| placement Which corner the badge sits on. Its center lands on that corner. Mirrored under RTL (right means the end side). | "top-right" | "top-left" | "bottom-right" | "bottom-left" | "top-right" |
| overlap `circular` pulls the badge in to sit on the edge of a round child such as an avatar. | "rectangular" | "circular" | "rectangular" |
| style Style for the outer container. | StyleProp<ViewStyle> | — |
Motion and haptics
- Appears with the bouncy spring from zero scale; at zero (or when its content goes away) it shrinks out with a fast timing, then unmounts.
- A changed count or label pops: a quick swell to 1.3, then the bouncy spring back.
- `pulse` sends a ring outward from the dot every 1.4 s, for live and online states.
- Reduced motion: no pop, no scale and no pulse; the badge fades in and out.
- Haptic: None: a badge reports a change, it does not confirm an action.
- All animation runs on the UI thread. React renders only when the count, label or visibility changes.
- BadgeAnchor measures its child once per real size change (for circular overlap) and never re-lays it out.
Accessibility
- Read as text: the label, the count, or 'More than 99' past the cap; override with accessibilityLabel.
- Pass `decorative` when the element it belongs to already says the count, so it is not read twice.
- The badge never takes touches; BadgeAnchor's child keeps its own role and label.
- Status meaning never relies on color alone: pair a tone with a label or with the anchored element's label.
- On the web a badge is role img named by its label, and a decorative badge is aria-hidden.
Notes for AI
- Default tone is danger with the solid variant: the unread count look. Use tone='accent' or 'neutral' with variant='soft' for status that is not urgent.
- Pass a number to count, not a string; the cap is max (default 99) and renders as '99+'.
- A badge with count 0 hides itself unless showZero is set. Keep it mounted and pass 0 so it can animate out.
- BadgeAnchor hugs its child (alignSelf flex-start). Use overlap='circular' for round avatars and a ring token that matches the surface behind the child.
- dot alone shows a dot (while count > 0 if a count is given); dot with label shows a status dot before the text.
Related
A layered icon-only button whose surface and icon sink on touch down and release on different springs; it runs async actions itself, carries a badge and shows a hint on long press.
Filter, choice and input chips: selecting one fills it with the accent while a check draws itself in, removing one shrinks it away, and a ChipGroup in single mode moves one elastic highlight from chip to chip.
A number whose digits roll in the direction of change, with digits that open and close in place and currency signs that glide instead of jumping.