verified

Badges

Status indicator for counts or labels.

Badges Examples

Badge/counter component that can be attached to an icon or any target node. Includes overlay/inline layouts, labels, pills, and interactive behavior.

Overlay basics (icon target)

Overlay places the badge on top of the target icon.

Live preview

Icon + quantity / dot / capped quantity

import Badges from "@/_components/badges/Badges";

export default function Example() {
  return (
    <>
      <Badges
        showInfo
        baseNumber={12}
        baseLimit={99}
        iconName="Notifications"
        variant="secondary"
      />

      <Badges
        showInfo
        notQuantity
        baseNumber={0}
        baseLimit={0}
        iconName="Mail"
        variant="primary"
      />

      <Badges
        showInfo
        baseNumber={120}
        baseLimit={99}
        iconName="Chat"
        variant="danger"
      />
    </>
  );
}

Inline (text targets)

Inline flows the badge next to the target. Useful for tabs, chip, filters, etc.

Live preview

Inline counters for text targets

import Badges from "@/_components/badges/Badges";

const targets = [
  { id: "inbox", label: "Inbox", count: 3 },
  { id: "alerts", label: "Alerts", count: 32 },
  { id: "updates", label: "Updates", count: 999 },
] as const;

export default function Example() {
  return (
    <>
      {targets.map((t) => (
        <Badges
          key={t.id}
          mode="inline"
          target={t.label}
          targetType="text"
          showInfo
          baseNumber={t.count}
          baseLimit={99}
          variant="danger"
        />
      ))}
    </>
  );
}

Labels (start/end)

Use label + labelPosition for horizontal label placement.

Live preview

AlertsInbox

Label at start / end

import Badges from "@/_components/badges/Badges";

export default function Example() {
  return (
    <>
      <Badges
        showInfo
        baseNumber={3}
        baseLimit={9}
        iconName="Notifications"
        label="Alerts"
        labelPosition="start"
        variant="secondary"
      />

      <Badges
        showInfo
        baseNumber={12}
        baseLimit={99}
        iconName="Mail"
        label="Inbox"
        labelPosition="end"
        variant="primary"
      />
    </>
  );
}

Sizes

Size affects badge offsets/tokens through CSS.

Live preview

Small / Medium / Large

import Badges from "@/_components/badges/Badges";

export default function Example() {
  return (
    <>
      <Badges showInfo baseNumber={3} baseLimit={9} iconName="Notifications" size="small" />
      <Badges showInfo baseNumber={3} baseLimit={9} iconName="Notifications" size="medium" />
      <Badges showInfo baseNumber={3} baseLimit={9} iconName="Notifications" size="large" />
    </>
  );
}

Variants

Variants control badge tone (and pill tone when pill is enabled).

Live preview

All variants

import Badges from "@/_components/badges/Badges";

export default function Example() {
  return (
    <>
      <Badges showInfo baseNumber={1} baseLimit={9} iconName="Notifications" variant="primary" />
      <Badges showInfo baseNumber={2} baseLimit={9} iconName="Notifications" variant="secondary" />
      <Badges showInfo baseNumber={3} baseLimit={9} iconName="Notifications" variant="tertiary" />
      <Badges showInfo baseNumber={4} baseLimit={9} iconName="Notifications" variant="success" />
      <Badges showInfo baseNumber={8} baseLimit={9} iconName="Notifications" variant="warning" />
      <Badges showInfo baseNumber={12} baseLimit={9} iconName="Notifications" variant="danger" />
      <Badges showInfo baseNumber={99} baseLimit={9} iconName="Notifications" variant="info" />
    </>
  );
}

Vertical layout (labelFlow)

For vertical layouts you can place the label above or below the target.

Live preview

PhotosPhotos

labelFlow before vs after

import Badges from "@/_components/badges/Badges";

export default function Example() {
  return (
    <>
      <Badges
        iconName="Image"
        label="Photos"
        showInfo
        baseNumber={999}
        baseLimit={99}
        orientation="vertical"
        labelFlow="before"
        variant="info"
      />

      <Badges
        iconName="Image"
        label="Photos"
        showInfo
        baseNumber={999}
        baseLimit={99}
        orientation="vertical"
        labelFlow="after"
        variant="info"
      />
    </>
  );
}

Pill mode: "target"

Wraps only the target in a pill. Common for "mobile" app-bar style items.

Selection is external: control isSelected with your own state.

Interactivity is inferred from onChange + disabled.

Live preview

HomeBrowseRadioAlerts

Controlled selection (pill=target)

import { useState } from "react";
import Badges from "@/_components/badges/Badges";

export default function Example() {
  const [selected, setSelected] = useState<"home" | "search" | "radio" | "alerts">("home");

  return (
    <>
      <Badges
        iconName="Home"
        label="Home"
        showInfo
        baseNumber={3}
        baseLimit={9}
        orientation="vertical"
        labelFlow="before"
        pill="target"
        variant="primary"
        isSelected={selected === "home"}
        onChange={() => setSelected("home")}
      />

      <Badges
        iconName="Search"
        label="Browse"
        showInfo={false}
        baseNumber={0}
        baseLimit={0}
        orientation="vertical"
        labelFlow="before"
        pill="target"
        variant="secondary"
        isSelected={selected === "search"}
        onChange={() => setSelected("search")}
      />

      <Badges
        iconName="Radio"
        label="Radio"
        showInfo
        notQuantity
        baseNumber={0}
        baseLimit={0}
        orientation="vertical"
        labelFlow="after"
        pill="target"
        variant="tertiary"
        isSelected={selected === "radio"}
        onChange={() => setSelected("radio")}
      />

      <Badges
        iconName="Notifications"
        label="Alerts"
        showInfo
        baseNumber={32}
        baseLimit={99}
        orientation="vertical"
        labelFlow="after"
        pill="target"
        variant="danger"
        isSelected={selected === "alerts"}
        onChange={() => setSelected("alerts")}
      />
    </>
  );
}

Pill mode: "all"

Wraps the whole anchor in a pill. Useful for fullscreen / horizontal navigation.

Live preview

TasksWarningsInfoLibrary

Controlled selection (pill=all)

import { useState } from "react";
import Badges from "@/_components/badges/Badges";

export default function Example() {
  const [selected, setSelected] = useState<"tasks" | "warnings" | "info" | "library">("tasks");

  return (
    <>
      <Badges
        iconName="Task"
        label="Tasks"
        labelPosition="end"
        showInfo
        baseNumber={12}
        baseLimit={99}
        pill="all"
        variant="success"
        isSelected={selected === "tasks"}
        onChange={() => setSelected("tasks")}
      />

      <Badges
        iconName="Warning"
        label="Warnings"
        labelPosition="end"
        showInfo
        baseNumber={8}
        baseLimit={9}
        pill="all"
        variant="warning"
        isSelected={selected === "warnings"}
        onChange={() => setSelected("warnings")}
      />

      <Badges
        iconName="Info"
        label="Info"
        labelPosition="end"
        showInfo
        baseNumber={1}
        baseLimit={9}
        pill="all"
        variant="info"
        isSelected={selected === "info"}
        onChange={() => setSelected("info")}
      />

      <Badges
        iconName="LibraryBooks"
        label="Library"
        labelPosition="end"
        showInfo={false}
        baseNumber={0}
        baseLimit={0}
        pill="all"
        variant="secondary"
        isSelected={selected === "library"}
        onChange={() => setSelected("library")}
      />
    </>
  );
}

Custom target (ReactNode)

When target is provided, it takes priority over iconName.

Live preview

Target as ReactNode

import Badges from "@/_components/badges/Badges";
import Icon from "@/_components/icon/Icon";

export default function Example() {
  return (
    <>
      <Badges
        mode="overlay"
        target={
          <span style={{ display: "inline-flex", alignItems: "center", gap: 6 }}>
            <Icon name="Flight" size="small" />
            <span>Trips</span>
          </span>
        }
        targetType="text"
        variant="warning"
        baseNumber={32}
        baseLimit={99}
        showInfo
        pill="all"
        onChange={() => {}}
      />

      <Badges
        mode="inline"
        target={<span>Plain text target</span>}
        targetType="text"
        variant="info"
        baseNumber={1}
        baseLimit={9}
        showInfo
        notQuantity
      />
    </>
  );
}

Interactivity (controlled)

When you pass onChange (and not disabled), the root becomes keyboard-focusable and triggers on Click / Enter / Space.

Live preview

Notifications: ON
Tip: interactivity is inferred from onChange + disabled.

Toggle dot vs count (controlled)

import { useState } from "react";
import Badges from "@/packages/ui/src/_components/badges/Badges";

export default function Example() {
  const [enabled, setEnabled] = useState(true);

  return (
    <Badges
      showInfo
      iconName="Notifications"
      label={enabled ? "Notifications: ON" : "Notifications: OFF"}
      labelPosition="end"
      notQuantity={!enabled}
      baseNumber={enabled ? 3 : 0}
      baseLimit={9}
      variant="secondary"
      onChange={() => setEnabled((v) => !v)}
    />
  );
}

Edge cases (numbers)

Use these cases to stress-test rendering and caps.

Numbers edge cases

import Badges from "@/_components/badges/Badges";

export default function Example() {
  return (
    <>
      <Badges showInfo iconName="Notifications" baseNumber={0} baseLimit={9} />
      <Badges showInfo iconName="Notifications" baseNumber={-1} baseLimit={9} />
      <Badges showInfo iconName="Notifications" baseNumber={9} baseLimit={0} />

      <Badges showInfo iconName="Mail" baseNumber={10} baseLimit={9} />
      <Badges showInfo iconName="Mail" baseNumber={120} baseLimit={99} />
    </>
  );
}

On this page

Badges Props

Badges — Props
PropTypeRequiredDefaultDescription
showInfobooleanYesEnables rendering the badge node. When false, no badge/dot is rendered.
notQuantitybooleanNofalseIf true, renders a small dot instead of a numeric counter.
baseNumbernumberYesCurrent value for the counter (used when notQuantity is false).
baseLimitnumberYesMax visible value. If baseNumber > baseLimit, it renders `${baseLimit}+`.
iconNamestringNoRenders an Icon as the target. Ignored if target is provided.
targetReact.ReactNodeNoCustom target node (e.g., text, avatar, etc.). Takes priority over iconName.
targetType"icon" | "text"NoIf not provided, inferred: "icon" if iconName exists, else "text" if target exists, else "icon".
size"small" | "medium" | "large"No"medium"Size preset (affects overlay offsets/tokens via CSS).
variant"primary" | "secondary" | "tertiary" | "success" | "warning" | "danger" | "info"NoBadge tone. Also used as pill tone when pill is enabled.
mode"overlay" | "inline"No"overlay"Overlay places the badge on top of the target. Inline flows next to it.
labelstringNoOptional label text.
labelPosition"start" | "end"NoLabel placement for horizontal layout (only when label is provided).
orientation"horizontal" | "vertical"No"horizontal"Stack direction for label/target layout.
labelFlow"after" | "before"No"after"For vertical layouts: before puts the label above the target.
pill"none" | "target" | "all"No"none"Enables pill styling: wrap only the target or the whole anchor.
isSelectedbooleanNofalseAdds selected modifier (useful with pill styling).
disabledbooleanNofalseDisables interactivity and sets aria-disabled.
onChange(...args: any) => any | voidNoWhen provided (and not disabled), the badge becomes interactive: role='button', tabIndex=0, click + Enter/Space.
interactivebooleanNoCurrently not used by the component (interactivity is inferred from onChange + disabled).
classNamestringNoExtra classes for the root.