Spec version v0.9.0

Badge

Purpose

A Badge is a compact, non-interactive label used to communicate status, category, count, or classification at a glance. Use a Badge to annotate an entity with a brief, scannable piece of metadata — a build status, a permission level, a lifecycle state, a version tag. Do NOT use a Badge to convey detailed explanations; that is the job of a Tooltip or an Alert. Do NOT use a Badge as a button or navigation target. If the label must be interactive, use a Tag with a remove action or a Button.

Badges appear inline within table cells, card headers, page headers, and list items. They are not standalone elements — they always annotate something else.

Anatomy

A Badge is a single inline element containing:

  • Label text (required): a short string, typically one to three words. Badge text is uppercase, 12px, medium weight, and wide letter-spaced — this treatment is inherited automatically from the badge component and must not be overridden per-instance.
  • Leading icon (optional): a small status icon (16px or smaller) placed to the left of the label. Use only Material Symbols Rounded icons. Icons inside badges inherit the badge text color. Use an icon when the status has a universally understood symbol (a check for success, an X for error, an exclamation for warning) — the icon reinforces the label, it does not replace it.
  • Trailing dismiss button (optional): a small icon button (X icon) to the right of the label, used only in dismissible or removable tag contexts. The dismiss button is a separate interactive element with its own accessible label. Do NOT add a dismiss button to read-only status badges.

A Badge does not contain arbitrary content. No multi-line text, no images, no nested badges.

Variants

Badges combine a visual style (subtle, outline, or strong) with a color (neutral, brand, success, warning, error, info). The combination is chosen by asking: how much visual weight should this status label carry?

subtle (default)

Tinted background in the {color}-subtle background token, no border, colored text in the matching {color} text token, pill radius. Subtle is the default and most common badge style. Use it for ambient status labeling where the badge should be noticeable but not dominant — state labels in a table column, role labels in a list, environment tags in a card header. The tinted background clearly communicates semantic color without overwhelming the surrounding content.

outline

Transparent background, 1px border in the variant's border token, colored text, pill radius. Use outline badges when the badge appears on a colored or tinted background where a filled/tinted background would blend in or fight with the surface. Also use outline for a secondary status label when a subtle badge is already present on the same element and visual differentiation is needed.

strong (filled)

Solid filled background in the variant's primary color token, white text (--force-color-text-on-primary), pill radius. Use strong badges sparingly, only when the status demands maximum visual attention within a dense interface — a critical error count, an overdue count, an "Action required" state. Because strong badges carry the most visual weight in the badge family, limit their use to one or two per screen. Overusing strong badges causes the entire interface to feel noisy and reduces the signal value of the strong treatment.

Color variants

neutral (gray): Use for states that are neither positive nor negative — draft, pending review, inactive, archived, unassigned. Background and text use neutral-scale tokens. This is the correct choice when status is not yet known or when an entity is in a non-error, non-success liminal state.

brand (primary/indigo): Use for brand-associated classifications — for example, marking a recommended option, a featured item, or a "Pro" feature tier. Do NOT use brand color for all primary items indiscriminately; it should mark something specifically affiliated with the Perforce brand identity.

success (green): Use for completed, healthy, passing, active, or confirmed states. Examples: "Active", "Passed", "Deployed", "Connected". Use only when the state genuinely represents a successful or healthy outcome.

warning (orange): Use for states requiring attention but not immediate action — degraded, at risk, expiring soon, stale. Examples: "Expiring", "Degraded", "At risk". Do NOT use warning for purely informational states; warning implies something should be checked.

error (cranberry): Use for failed, blocked, rejected, critical, or invalid states. Examples: "Failed", "Blocked", "Rejected", "Overdue". Error badges signal that user action or system attention is required.

info (blue): Use for purely informational classifications with no urgency — "In progress", "Scheduled", "Preview", "Beta". Info is neutral in tone; it communicates state without implying positive or negative valence.

Sizes

Badges are available in four sizes. Size selection should match the density of the surrounding context, not arbitrary preference.

  • xs (20px height): Used in highly compressed contexts such as avatar overlays, inline code annotations, or dense data tables where even the sm size adds too much visual weight. Text is --force-font-size-xs (12px). Horizontal padding is --force-spacing-2 (8px).
  • sm (24px height): The standard size for table cells and card header annotations. Text is --force-font-size-xs (12px). Horizontal padding is --force-spacing-2 (8px).
  • md (28px height): Use in page headers, entity title rows, and standalone metadata areas where slightly more presence is appropriate. Text is --force-font-size-xs (12px). Horizontal padding is --force-spacing-3 (12px).
  • lg (32px height): Use sparingly, for prominent standalone status indicators in dashboards or large-format layouts where the badge must be visible from a greater scanning distance. Text is --force-font-size-sm (14px). Horizontal padding is --force-spacing-3 (12px).

The default size is sm. Do not use md or lg as defaults simply to fill space — badges should not be sized up to match button height or other taller elements. If alignment is needed, use vertical-alignment rules, not oversized badges.

States

Badges are non-interactive by default and have no hover, active, or focus states.

Default: Badge renders at full color and opacity per its variant and color combination. This is the only visual state for a read-only badge.

Disabled (rare): When a badge annotates a disabled entity (e.g., a feature that is unavailable in the current tier), apply opacity: --force-opacity-disabled (0.5) to the badge. Do NOT substitute a neutral badge for a disabled status badge — the color still carries meaning, it is simply de-emphasized.

Dismissible trailing button — hover: The dismiss icon button inside the badge transitions to a slightly stronger text color on hover. The badge container itself does not change.

Dismissible trailing button — focus: The dismiss icon button receives a focus ring using --force-shadow-focus-ring and --force-color-border-focus. Focus applies to the button only, not the badge container.

Behavior

Badges are <span> elements by default. They render inline within their parent flow. Do not add click handlers to the badge container. If the badge must be interactive as a whole unit (e.g., clicking it filters a list), use a different element with a proper button role, or wrap the badge in a button and handle the visual treatment accordingly.

Multiple badges in a row should be separated by --force-spacing-2 (8px) gap. When three or more badges would appear on a single entity, prefer truncating to two visible badges plus an overflow count indicator (e.g., "+3 more") rather than wrapping to multiple lines.

Badge text is always rendered as uppercase with --force-font-letter-spacing-wide (0.025em) tracking. Do NOT write badge labels in sentence case or title case in the component — the text transform is applied by the component. Write the source label in whatever case makes the prop value human-readable (e.g., label="active" not label="ACTIVE").

Accessibility

Badges rendered as <span> elements are read by screen readers as inline text in document order, which is the correct behavior for status labels. No additional ARIA role is required for a static badge.

When a badge communicates critical status that would otherwise be missed (e.g., an error badge in a data table column that a screen reader might skip over), consider adding a visually-hidden prefix to the text content (e.g., "Status: Failed") so the spoken label includes context.

Dismissible badges must have an accessible label on the dismiss button. The label must identify what is being dismissed — not just "Close" or "Remove" — for example: aria-label="Remove 'Active' filter". Generic dismiss labels are not acceptable when multiple dismissible badges appear on the same page.

Color alone must never be the only signal. The text label is always present alongside color. Do NOT create an icon-only badge without a text label — that pattern is inaccessible.

Composition

Badges appear most commonly in:

  • Table cells: a single badge per cell in a "Status" column, typically sm size, subtle variant
  • Card headers: one or two badges in the CardAction slot or beside the CardTitle, typically sm or xs size
  • Page headers (PageHeader): entity lifecycle badges beside the entity title, typically md size
  • List items: inline beside item names to annotate classification, sm size
  • Form select options: inline in a Select or Combobox dropdown to label option states

Do not place a Badge inside a Button. Do not use a Badge as the label for a Toggle or Switch. Do not put a Badge inside another Badge.

When a Badge appears beside a heading, align it to the vertical center of the heading using align-items: center on the flex row. Do not allow the badge to stretch to heading height.

Guidance

Use the most semantically appropriate color variant, not the one that looks nicest in context. If a build has passed, use success. If a deployment is in progress, use info. Choosing warning for a "pending" state because orange looks more interesting than gray is a misuse of the semantic color system.

Do NOT use the strong (filled) variant as the default badge style. Strong badges are intended for states that demand maximum attention. If every badge on a page is strong, the visual hierarchy collapses and the signal value of strong is lost.

Do NOT create custom badge colors by overriding token values. The badge color set (neutral, brand, success, warning, error, info) covers all legitimate use cases in the Force UI system. If a new color is genuinely needed, raise it as a token system request — do not implement a one-off color on a badge.

Do NOT shrink badge font size below --force-font-size-xs (12px). This is the minimum readable size in the system.

Common mistakes to avoid:

  • Do NOT label a "Pending" state with warning (orange) — pending is neutral. Use neutral (gray) or info (blue).
  • Do NOT use brand (indigo) badges for interactive-feeling labels — they are easily confused with links or buttons in the brand color.
  • Do NOT stack multiple same-color subtle badges in the same row — the visual distinction disappears. Vary color or variant.

Common Token Mistakes

Zero-contrast brand badge: Using --force-color-text-primary-brand (indigo-500) on --force-color-bg-primary (indigo-500) produces zero contrast — the text is invisible. The token name sounds like "the primary brand text color" and is the intuitive first choice for brand badge text, but both tokens resolve to the same indigo value. For strong (filled) brand badges, always use --force-color-text-on-primary (white) on --force-color-bg-primary. Reserve --force-color-text-primary-brand for subtle and outline brand badges on neutral backgrounds.

Wrong foreground for subtle badges: Using --force-color-text-on-primary (white) on a subtle badge background like --force-color-bg-primary-subtle (indigo-50). White text is nearly invisible on a light tinted background. Subtle badges use the full-saturation text token (--force-color-text-primary-brand for brand, --force-color-text-success for success, etc.).

Mixing variant and color independently: Choosing a background from one variant (e.g., strong brand background) and a text color from another (e.g., subtle success text). Always use the complete token set for a single variant+color combination as listed in the Token Usage section below.

Token Usage

subtle variant

  • Background: --force-color-bg-{variant}-subtle (e.g., --force-color-bg-success-subtle)
  • Text: --force-color-text-{variant} (e.g., --force-color-text-success)
  • Border: none
  • Border radius: --force-radius-badge (pill / 9999px)

For neutral/gray subtle:

  • Background: --force-color-bg-muted
  • Text: --force-color-text-secondary

For brand subtle:

  • Background: --force-color-bg-primary-subtle
  • Text: --force-color-text-primary-brand

outline variant

  • Background: transparent
  • Text: --force-color-text-{variant} (same as subtle)
  • Border: 1px solid --force-color-border-{variant} (e.g., --force-color-border-success)
  • Border radius: --force-radius-badge

For neutral/gray outline:

  • Border: 1px solid --force-color-border-default
  • Text: --force-color-text-secondary

strong (filled) variant

  • Background: --force-color-bg-{variant} (e.g., --force-color-bg-primary, --force-color-bg-destructive, --force-color-bg-success, --force-color-bg-warning, --force-color-bg-info)
  • Text: the matching on-* token:
    • brand → --force-color-text-on-primary (white)
    • destructive → --force-color-text-on-primary (white)
    • success → --force-color-text-on-success (white)
    • warning → --force-color-text-on-warning (black — warning-500 is too light for white text)
    • info → --force-color-text-on-info (white)
  • Border: none
  • Border radius: --force-radius-badge

Typography

  • Font size: --force-font-size-xs (12px) for xs/sm/md sizes; --force-font-size-sm (14px) for lg size
  • Font weight: --force-font-weight-medium (500)
  • Text transform: uppercase (applied by component)
  • Letter spacing: --force-font-letter-spacing-wide (0.025em)
  • Line height: --force-font-line-height-tight (1.25)

Spacing

  • xs/sm horizontal padding: --force-spacing-2 (8px)
  • md/lg horizontal padding: --force-spacing-3 (12px)
  • Icon-to-label gap: --force-spacing-1 (4px)
  • Gap between multiple badges: --force-spacing-2 (8px)

Disabled state

  • Opacity: --force-opacity-disabled (0.5) applied to the badge container