@xsolla/xui-badge
v0.216.1
Published
A cross-platform React badge component for counts, status dots, and small labels. Sizes `xs` and `sm` render as content-less dots; larger sizes accept text and icons.
Readme
Badge
A cross-platform React badge component for counts, status dots, and small labels. Sizes xs and sm render as content-less dots; larger sizes accept text and icons.
A small indicator dot that signals a new, active, or attention-requiring state. Renders as a filled circle or square at a very small fixed size, with optional content (icon or label) and an optional stroke border. Positioned absolutely over another element — most commonly on an Avatar, Icon button, or navigation icon. Unlike Tag, Badge carries no label by default and communicates through colour, shape, and position alone.
When to use
- To signal an unread or unseen state on an Avatar, Icon button, or navigation item — a notification dot
- To indicate online/offline presence on an Avatar
- To mark a new feature, new content, or an attention-requiring state without interrupting the flow
- As a minimal indicator when a count or label would add too much visual noise (use a numeric counter badge in that case)
When not to use
- When a specific count needs to be shown — use a numeric counter chip instead (e.g. inside Tabs or on an Icon button)
- For status labels with text — use a Tag or StatusDropdown
- As a standalone element on a page with no parent component to anchor it to
- When the state it signals needs to be explained — always pair with a tooltip or an aria-label
Content guidelines
- Plain dot (default) — no text or icon. Use in the vast majority of cases. The parent*'s context and the Badge'*s colour communicate the meaning.
- Icon inside — use only when the icon is immediately recognisable (e.g. ✓ for verified, ★ for featured). Keep to 1 icon, centred. Only at XL or L sizes.
- Label inside — use only for a very short string: a single digit ("3"), an abbreviation ("en"), or a tier indicator ("A"). Keep to 1–3 characters. Only at XL or L sizes.
Behaviour guidelines
- Visibility — show the Badge only when the state it signals is active. Remove it (unmount or hide with visibility: hidden) when the state is no longer true — e.g. once the user has read all notifications, remove the notification Badge from the Avatar.
- Animated entrance — optionally animate the Badge's appearance with a scale-in animation (transform: scale(0) → scale(1)) to draw attention to the new state without being distracting. Keep the animation under 200ms. Respect prefers-reduced-motion — use an instant appearance for users who prefer reduced motion.
- Real-time updates — if the Badge reflects a live count or state (e.g. presence), update it in real time without remounting the parent component. Change only the Badge's visibility or tone.
- Stacking — do not place two Badges on the same parent element simultaneously. If multiple states must be signalled, use the Badge for the highest-priority state only.
- Colour alone — Badge communicates state primarily through colour. Always pair with a tooltip or aria-label on the parent element to describe the state for users who cannot perceive colour.
Accessibility
- Badge itself has no accessible role — it is a visual decorator. All accessibility information must be on the parent element.
- The parent element (Avatar, Icon button, navigation icon) must have aria-label that includes the Badge's meaning — e.g. aria-label="Alex Johnson, online" or aria-label="Notifications, 3 unread" or aria-label="Settings, update available".
- Do not rely on the Badge's colour alone to communicate state (WCAG 1.4.1 Use of Colour). The aria-label on the parent provides the non-colour signal.
- When the Badge appears or disappears dynamically, announce the change via aria-live="polite" on a visually hidden region — e.g. "Alex Johnson is now online" or "New notifications".
- The Badge element itself must have aria-hidden="true" so screen readers do not attempt to read the dot as content.
- When Icon=true or Label=true, the badge may carry readable content, but the meaning is still communicated via the parent's aria-label — keep aria-hidden="true" on the Badge and include the relevant information in the parent label.
Installation
npm install @xsolla/xui-badgeImports
import { Badge } from "@xsolla/xui-badge";Quick start
import * as React from "react";
import { Badge } from "@xsolla/xui-badge";
export default function QuickStart() {
return <Badge tone="alert">5</Badge>;
}API Reference
<Badge>
| Prop | Type | Default | Description |
| ------------- | ------------------------------------------------------------------------------------------------------- | --------- | ------------------------------------------------------------------------------------------------------------- |
| testID | string | — | Test ID for testing frameworks. On web this renders as data-testid; on React Native it renders as testID. |
| children | ReactNode | — | Badge content. Numeric values greater than 999 are clamped to 999+. Ignored for xs/sm sizes. |
| icon | ReactNode | — | Icon rendered alongside content. Ignored for xs/sm sizes. |
| size | "xl" \| "lg" \| "md" \| "sm" \| "xs" | "md" | Badge size. xs and sm are dot-only. |
| tone | "primary" \| "secondary" \| "brand" \| "brandExtra" \| "success" \| "warning" \| "alert" \| "neutral" | "alert" | Colour tone. |
| square | boolean | false | Use a small square radius (4px) instead of pill/circle. |
| showStroke | boolean | false | Render a 1px border in the page background colour, useful when overlaid on imagery. |
| aria-label | string | — | Accessible label. |
| aria-hidden | boolean | auto | Defaults to true for content-less dots without aria-label. |
Inherits ThemeOverrideProps (themeMode, themeProductContext).
Examples
Tones
import * as React from "react";
import { Badge } from "@xsolla/xui-badge";
export default function BadgeTones() {
return (
<div style={{ display: "flex", gap: 16 }}>
<Badge tone="primary">P</Badge>
<Badge tone="secondary">S</Badge>
<Badge tone="brand">B</Badge>
<Badge tone="brandExtra">X</Badge>
<Badge tone="success">9</Badge>
<Badge tone="warning">!</Badge>
<Badge tone="alert">3</Badge>
<Badge tone="neutral">N</Badge>
</div>
);
}Sizes
import * as React from "react";
import { Badge } from "@xsolla/xui-badge";
export default function BadgeSizes() {
return (
<div style={{ display: "flex", gap: 16, alignItems: "center" }}>
<Badge size="xs" tone="alert" aria-label="Unread" />
<Badge size="sm" tone="alert" aria-label="Unread" />
<Badge size="md" tone="alert">
5
</Badge>
<Badge size="lg" tone="alert">
5
</Badge>
<Badge size="xl" tone="alert">
5
</Badge>
</div>
);
}With icon
import * as React from "react";
import { Badge } from "@xsolla/xui-badge";
import { Heart } from "@xsolla/xui-icons-base";
export default function BadgeWithIcon() {
return (
<Badge tone="warning" icon={<Heart />}>
Featured
</Badge>
);
}Square
import * as React from "react";
import { Badge } from "@xsolla/xui-badge";
export default function SquareBadge() {
return (
<Badge tone="brand" square>
NEW
</Badge>
);
}Notification count
import * as React from "react";
import { Badge } from "@xsolla/xui-badge";
export default function NotificationCount() {
const [count, setCount] = React.useState(12);
return (
<button
onClick={() => setCount((c) => c + 1)}
style={{ position: "relative" }}
>
Inbox
<span style={{ position: "absolute", top: -8, right: -8 }}>
<Badge tone="alert">{count}</Badge>
</span>
</button>
);
}With stroke
import * as React from "react";
import { Badge } from "@xsolla/xui-badge";
export default function BadgeStroke() {
return (
<div style={{ background: "#222", padding: 16 }}>
<Badge tone="brand" showStroke>
Pro
</Badge>
</div>
);
}Accessibility
- The root has
role="status"so changes are announced politely. - Dot-only badges (
xs,sm) without anaria-labeldefault toaria-hidden="true". - Provide
aria-labelfor badges that convey state visually only (such as a coloured dot beside an avatar).
