Component
Badge
Badges attach a concise status label or numeric count to another element. Use Badge for short text tags and NumberBadge for counts — both render six semantic variants from the --components-badge-tag-* tokens.
Playground
Kind
Variant
Size
Label
Leading dot
Dismiss icon
Preview
Badge Label
JSX
<Badge
variant="primary"
size="sm"
label="Badge Label"
showLeftIcon
showRightIcon
/>Props
The full BadgeProps API (text / label badge). Props marked Required have no default.
| Prop | Type | Default | Description |
|---|---|---|---|
variant | 'primary' | 'error' | 'warning' | 'success' | 'information' | 'neutral' | 'primary' | Badge intent — controls the color tokens. |
size | 'sm' | 'md' | 'lg' | 'sm' | Height and typography scale. 'sm' uses L1 12px; 'md'/'lg' use B2 14px. |
label | string | 'Badge Label' | Text shown inside the badge (1–3 words). Truncates on overflow. |
showLeftIcon | boolean | true | Show the leading 8px dot indicator (decorative). |
showRightIcon | boolean | true | Show the trailing dismiss (close) control. |
onDismiss | () => void | — | Called when the dismiss control is activated. |
className | string | — | Layout-only classes (margin, positioning). |
Props
The full NumberBadgeProps API (circular numeric badge).
| Prop | Type | Default | Description |
|---|---|---|---|
variant | 'primary' | 'error' | 'warning' | 'success' | 'information' | 'neutral' | 'primary' | Badge intent — controls the color tokens. |
size | 'xs' | 'sm' | 'md' | 'lg' | 'xs' | Diameter (20 / 24 / 28 / 36px) and typography scale. |
support | 'default' | 'subtle' | 'subtle' | 'default' = solid fill on white text; 'subtle' = light fill on tinted text. |
label | string | '9' / '9+' / '99+' (by size) | Numeric text, e.g. "9", "9+", "99+". Defaults per size; pass an empty string for a pure dot indicator. |
className | string | — | Layout-only classes (margin, absolute positioning over an icon/avatar). |
Usage guidelines
When to use
- Attach a short status label to an item — "New", "Draft", "Live" — with a Badge.
- Show an unread count or notification total on a nav icon, tab, or avatar with a NumberBadge.
- Pair the variant colour with the label text so the intent stays clear without relying on colour alone.
When not to use
- Don't put long descriptive text in a badge — use an Alert or tooltip for messages.
- Don't make badges interactive (filtering, navigation) — they are display-only; use a Button or chip.
- Don't mix the two: use Badge for words and NumberBadge for counts — never one for the other.
nexus/components/badge.tsx
import { CloseOutlineIcon } from "nexus/icons";
export type BadgeVariant =
| "primary"
| "error"
| "warning"
| "success"
| "information"
| "neutral";
export type BadgeSize = "sm" | "md" | "lg";
export interface BadgeProps {
/** Badge intent — controls color tokens. */
variant?: BadgeVariant;
/** Height + typography scale. */
size?: BadgeSize;
/** Text shown inside the badge (1–3 words). Truncates on overflow. */
label?: string;
/** Show the leading 8px dot indicator (decorative). */
showLeftIcon?: boolean;
/** Show the trailing dismiss (close) control. */
showRightIcon?: boolean;
/** Called when the dismiss control is activated. */
onDismiss?: () => void;
/** Layout-only classes (margin, positioning). */
className?: string;
}
interface VariantStyle {
/** Subtle background fill. */
surface: string;
/** Label text color. */
text: string;
/** Leading dot fill. */
dot: string;
/** Dismiss icon `<path>` fill. */
iconFill: string;
}
// Static per-variant class strings — Tailwind only emits utilities it can see as
// literals, so these must never be assembled at runtime. Tokens live in
// nexus/styles/tokens.css (note the source "nuetral" spelling for neutral).
const VARIANTS: Record<BadgeVariant, VariantStyle> = {
primary: {
surface: "bg-[var(--components-badge-tag-color-primary-background-subtle)]",
text: "text-[var(--components-badge-tag-color-primary-text-primary)]",
dot: "bg-[var(--components-badge-tag-color-primary-icon-primary)]",
iconFill: "[&_path]:fill-[var(--components-badge-tag-color-primary-text-primary)]",
},
error: {
surface: "bg-[var(--components-badge-tag-color-error-background-subtle)]",
text: "text-[var(--components-badge-tag-color-error-text-primary)]",
dot: "bg-[var(--components-badge-tag-color-error-icon-primary)]",
iconFill: "[&_path]:fill-[var(--components-badge-tag-color-error-text-primary)]",
},
warning: {
surface: "bg-[var(--components-badge-tag-color-warning-background-subtle)]",
text: "text-[var(--components-badge-tag-color-warning-text-primary)]",
dot: "bg-[var(--components-badge-tag-color-warning-icon-primary)]",
iconFill: "[&_path]:fill-[var(--components-badge-tag-color-warning-text-primary)]",
},
success: {
surface: "bg-[var(--components-badge-tag-color-success-background-subtle)]",
text: "text-[var(--components-badge-tag-color-success-text-primary)]",
dot: "bg-[var(--components-badge-tag-color-success-icon-primary)]",
iconFill: "[&_path]:fill-[var(--components-badge-tag-color-success-text-primary)]",
},
information: {
surface: "bg-[var(--components-badge-tag-color-information-background-subtle)]",
text: "text-[var(--components-badge-tag-color-information-text-primary)]",
dot: "bg-[var(--components-badge-tag-color-information-icon-primary)]",
iconFill: "[&_path]:fill-[var(--components-badge-tag-color-information-text-primary)]",
},
neutral: {
surface: "bg-[var(--components-badge-tag-color-nuetral-background-subtle)]",
text: "text-[var(--components-badge-tag-color-nuetral-text-primary)]",
dot: "bg-[var(--components-badge-tag-color-nuetral-icon-primary)]",
iconFill: "[&_path]:fill-[var(--components-badge-tag-color-nuetral-text-primary)]",
},
};
interface SizeStyle {
/** Max width + padding (badge hugs its content; no min-width). */
box: string;
/** Label typography utility. */
text: string;
/** Dismiss icon pixel size. */
icon: number;
}
const SIZES: Record<BadgeSize, SizeStyle> = {
sm: { box: "max-w-[1600px] px-8 py-4", text: "typography-l1-medium-12", icon: 17 },
md: { box: "max-w-[1600px] px-8 py-4", text: "typography-b2-medium-14", icon: 20 },
lg: { box: "max-w-[1600px] px-8 py-6", text: "typography-b2-medium-14", icon: 20 },
};
const cx = (...classes: (string | false | undefined)[]) =>
classes.filter(Boolean).join(" ");
/**
* Nexus Badge — a short status label / tag with an optional leading dot and
* trailing dismiss control. Text-only counterpart to `NumberBadge`. Built to the
* Figma "M-Badge" frame and the `--components-badge-tag-*` tokens
* (instruction/nexus-design-system.md §6.3).
*/
export const Badge = ({
variant = "primary",
size = "sm",
label = "Badge Label",
showLeftIcon = true,
showRightIcon = true,
onDismiss,
className,
}: BadgeProps) => {
const v = VARIANTS[variant];
const s = SIZES[size];
return (
<span
className={cx(
"inline-flex items-center justify-center gap-8 rounded-4",
s.box,
v.surface,
className,
)}
>
{showLeftIcon && (
<span aria-hidden className={cx("size-8 shrink-0 rounded-full", v.dot)} />
)}
<span className={cx("min-w-0 truncate text-left", s.text, v.text)}>
{label}
</span>
{showRightIcon && (
<button
type="button"
onClick={onDismiss}
aria-label={`Remove ${label} badge`}
className="inline-flex shrink-0"
>
<CloseOutlineIcon width={s.icon} height={s.icon} className={v.iconFill} />
</button>
)}
</span>
);
};nexus/components/number-badge.tsx
export type NumberBadgeVariant =
| "primary"
| "error"
| "warning"
| "success"
| "information"
| "neutral";
export type NumberBadgeSize = "xs" | "sm" | "md" | "lg";
export type NumberBadgeSupport = "default" | "subtle";
export interface NumberBadgeProps {
/** Badge intent — controls color tokens. */
variant?: NumberBadgeVariant;
/** Diameter + typography scale. */
size?: NumberBadgeSize;
/** `default` = solid fill on white text; `subtle` = light fill on tinted text. */
support?: NumberBadgeSupport;
/** Numeric text, e.g. "9", "9+", "99+". Omit for a pure dot indicator. */
label?: string;
/** Layout-only classes (margin, absolute positioning over an icon/avatar). */
className?: string;
}
interface VariantStyle {
/** Solid background + white text. */
default: string;
/** Subtle background + tinted text. */
subtle: string;
}
// Static per-variant class strings — never assemble Tailwind classes at runtime.
// Tokens live in nexus/styles/tokens.css (note the source "nuetral" spelling).
const VARIANTS: Record<NumberBadgeVariant, VariantStyle> = {
primary: {
default:
"bg-[var(--components-badge-tag-color-primary-background)] text-[var(--components-badge-tag-color-primary-text-white)]",
subtle:
"bg-[var(--components-badge-tag-color-primary-background-subtle)] text-[var(--components-badge-tag-color-primary-text-primary)]",
},
error: {
default:
"bg-[var(--components-badge-tag-color-error-background)] text-[var(--components-badge-tag-color-error-text-white)]",
subtle:
"bg-[var(--components-badge-tag-color-error-background-subtle)] text-[var(--components-badge-tag-color-error-text-primary)]",
},
warning: {
default:
"bg-[var(--components-badge-tag-color-warning-background)] text-[var(--components-badge-tag-color-warning-text-white)]",
subtle:
"bg-[var(--components-badge-tag-color-warning-background-subtle)] text-[var(--components-badge-tag-color-warning-text-primary)]",
},
success: {
default:
"bg-[var(--components-badge-tag-color-success-background)] text-[var(--components-badge-tag-color-success-text-white)]",
subtle:
"bg-[var(--components-badge-tag-color-success-background-subtle)] text-[var(--components-badge-tag-color-success-text-primary)]",
},
information: {
default:
"bg-[var(--components-badge-tag-color-information-background)] text-[var(--components-badge-tag-color-information-text-white)]",
subtle:
"bg-[var(--components-badge-tag-color-information-background-subtle)] text-[var(--components-badge-tag-color-information-text-primary)]",
},
neutral: {
default:
"bg-[var(--components-badge-tag-color-nuetral-background)] text-[var(--components-badge-tag-color-nuetral-text-white)]",
subtle:
"bg-[var(--components-badge-tag-color-nuetral-background-subtle)] text-[var(--components-badge-tag-color-nuetral-text-primary)]",
},
};
// Diameter + label typography per size. `md` (28px) has no spacing token, so it
// uses an arbitrary value (as the Avatar does for its 56px frame).
const SIZES: Record<NumberBadgeSize, { box: string; text: string }> = {
xs: { box: "size-20", text: "typography-l1-medium-12" },
sm: { box: "size-24", text: "typography-l1-medium-12" },
md: { box: "size-[28px]", text: "typography-l1-medium-12" },
lg: { box: "size-36", text: "typography-b2-medium-14" },
};
// Per the Figma M-Number-Badge frame: xs renders the single-digit label, sm/md
// the "9+" overflow, lg the "99+" overflow.
const DEFAULT_LABELS: Record<NumberBadgeSize, string> = {
xs: "9",
sm: "9+",
md: "9+",
lg: "99+",
};
const cx = (...classes: (string | false | undefined)[]) =>
classes.filter(Boolean).join(" ");
/**
* Nexus NumberBadge — a circular numeric count / presence indicator. Numeric
* counterpart to `Badge`. Built to the Figma "M-Number-Badge" frame and the
* `--components-badge-tag-*` tokens (instruction/nexus-design-system.md §6.3).
*/
export const NumberBadge = ({
variant = "primary",
size = "xs",
support = "subtle",
label = DEFAULT_LABELS[size],
className,
}: NumberBadgeProps) => {
const s = SIZES[size];
return (
<span
className={cx(
"inline-flex items-center justify-center rounded-full text-center",
s.box,
s.text,
VARIANTS[variant][support],
className,
)}
>
{label}
</span>
);
};