Badge

Free · MITReact Native · Expo

A 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.

View as Markdown
Try it on your phone
Scan to open this demo in your phone's browser and use it with your fingers.

Installation

$ npx shadcn@latest add https://goodui.dev/native/r/badge.json

Then 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

Use it for
  • 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).
Reach for something else
  • 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

PropTypeDefault
count
A count. Hidden at 0 unless `showZero`; above `max` it reads "99+".
number—
max
Highest count shown as a number. Default 99.
number99
showZero
Keep the badge visible with a count of 0.
booleanfalse
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.
booleanfalse
pulse
The dot breathes outward, for live or online states. Off under reduced motion.
booleanfalse
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 | falsefalse
decorative
Hide the badge from screen readers because the element it belongs to already says the count in its label.
booleanfalse
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

PropTypeDefault
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.