Skip to content

Type to search the docs, API, and blog.

↑↓ navigate↵ openesc close
Zazz 0.7 alpha
API navigation

API

Badge

A small label for status, counts, and categories.

Installation

pnpm dlx zazz-ui add badge
npx zazz-ui add badge
yarn dlx zazz-ui add badge
bunx zazz-ui add badge

Also adds fields, which it depends on. Run zazz-ui init first if the project has no zazz/ folder yet.

Examples

Default

<div
  style="
    --display: flex;
    --flex-wrap: wrap;
    --items: center;
    --gap: 4;
  "
>
  <button type="button" data-ui="badge">Category</button>
  <button type="button" data-ui="badge" data-badge-variant="primary">New</button>
  <button type="button" data-ui="badge" data-badge-variant="muted">Draft</button>
  <button type="button" data-ui="badge" data-badge-variant="ghost">Inline</button>
  <button type="button" data-ui="badge" data-badge-variant="info">Update</button>
  <button type="button" data-ui="badge" data-badge-variant="success">Paid</button>
  <button type="button" data-ui="badge" data-badge-variant="warning">Pending</button>
  <button type="button" data-ui="badge" data-badge-variant="destructive">Overdue</button>
  <button type="button" data-ui="badge" data-badge-variant="link">Posts</button>
</div>
/**
 * badge.css: Badge ([data-ui~="badge"])
 *
 * @layer      variables, components
 * @requires   _layers.css, _variables.css, _reset.css, _properties.css (--_*-resolved, --_focus-shadow), fields.css (--ui-field-* border/ring)
 * @uses       color-mix(), oklch(from …): variant hover/active tints
 * @uses       text-box: vertical trim (patchy browser support)
 * @tokens     --ui-badge-* (@layer variables; hooks are named after the utilities they back)
 * @consumedby combobox.css (selection tags)
 *
 * Every utility-backed declaration reads one chain, highest first:
 *   var(--_<utility>-resolved, var(--_badge-<utility>, var(--ui-badge-<utility>)))
 * States apply only to interactive badges (button, a[href]).
 */

@layer variables {
  :root {
    /* border + ring follow --ui-field-*; metrics stay independent (badges are smaller) */
    --ui-badge-ring-color: var(--ui-field-ring-color);

    --ui-badge-font-family: var(--font-family-body);
    --ui-badge-font-size: var(--font-size-xs);
    --ui-badge-font-weight: var(--font-weight-strong);
    --ui-badge-leading: 1;

    --ui-badge-bg: var(--color-background);
    --ui-badge-bg--hover: var(--color-muted);
    --ui-badge-bg--active: oklch(from var(--color-muted) l c h / calc(alpha - 0.02));

    --ui-badge-text: var(--color-foreground);
    --ui-badge-text--hover: var(--color-foreground);
    --ui-badge-text--active: var(--color-foreground);

    --ui-badge-border-width: var(--ui-field-border-width);
    --ui-badge-border-style: var(--ui-field-border-style);
    --ui-badge-border-color: var(--ui-field-border-color);
    --ui-badge-border-color--hover: var(--ui-field-border-color);
    --ui-badge-border-color--active: var(--ui-field-border-color);

    /* dual-mode: a scale number or a length */
    --ui-badge-gap: var(--spacing);
    --ui-badge-px: calc(var(--spacing) * 1.25);
    --ui-badge-h: calc(var(--spacing) * 5);
    --ui-badge-rounded: var(--radius-sm);

    --ui-badge-icon-size: calc(var(--spacing) * 3);

    /* never `none`: the focus ring layers into this box-shadow list */
    --ui-badge-shadow: 0 0 transparent;
  }
}

/* Variant privates: non-inheriting so a variant never cascades into a nested badge. */
@property --_badge-ring-color {
  syntax: "*";
  inherits: false;
}

@property --_badge-font-family {
  syntax: "*";
  inherits: false;
}

@property --_badge-font-size {
  syntax: "*";
  inherits: false;
}

@property --_badge-font-weight {
  syntax: "*";
  inherits: false;
}

@property --_badge-leading {
  syntax: "*";
  inherits: false;
}

@property --_badge-bg {
  syntax: "*";
  inherits: false;
}

@property --_badge-bg--hover {
  syntax: "*";
  inherits: false;
}

@property --_badge-bg--active {
  syntax: "*";
  inherits: false;
}

@property --_badge-text {
  syntax: "*";
  inherits: false;
}

@property --_badge-text--hover {
  syntax: "*";
  inherits: false;
}

@property --_badge-text--active {
  syntax: "*";
  inherits: false;
}

@property --_badge-border-width {
  syntax: "*";
  inherits: false;
}

@property --_badge-border-style {
  syntax: "*";
  inherits: false;
}

@property --_badge-border-color {
  syntax: "*";
  inherits: false;
}

@property --_badge-border-color--hover {
  syntax: "*";
  inherits: false;
}

@property --_badge-border-color--active {
  syntax: "*";
  inherits: false;
}

@property --_badge-gap {
  syntax: "*";
  inherits: false;
}

@property --_badge-px {
  syntax: "*";
  inherits: false;
}

@property --_badge-h {
  syntax: "*";
  inherits: false;
}

@property --_badge-rounded {
  syntax: "*";
  inherits: false;
}

@property --_badge-icon-size {
  syntax: "*";
  inherits: false;
}

@property --_badge-shadow {
  syntax: "*";
  inherits: false;
}

@layer ui.components {
  :where([data-ui~="badge"]) {
    font-family: var(
      --_font-family-resolved,
      var(--_badge-font-family, var(--ui-badge-font-family))
    );
    font-size: var(--_font-size-resolved, var(--_badge-font-size, var(--ui-badge-font-size)));
    font-weight: var(
      --_font-weight-resolved,
      var(--_badge-font-weight, var(--ui-badge-font-weight))
    );
    line-height: var(--_leading-resolved, var(--_badge-leading, var(--ui-badge-leading)));

    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;

    --_gap-len: var(--_gap-resolved, var(--_badge-gap, var(--ui-badge-gap)));
    --_gap-num: var(--_gap-resolved, var(--_badge-gap, var(--ui-badge-gap)));
    gap: calc(var(--_gap-len) + var(--_gap-num) * var(--spacing));

    --_h-len: var(--_h-resolved, var(--_badge-h, var(--ui-badge-h)));
    --_h-num: var(--_h-resolved, var(--_badge-h, var(--ui-badge-h)));
    block-size: calc(var(--_h-len) + var(--_h-num) * var(--spacing));
    min-inline-size: max-content;

    --_px-len: var(--_px-resolved, var(--_badge-px, var(--ui-badge-px)));
    --_px-num: var(--_px-resolved, var(--_badge-px, var(--ui-badge-px)));
    padding-inline: calc(var(--_px-len) + var(--_px-num) * var(--spacing));

    border-width: var(
      --_border-width-resolved,
      var(--_badge-border-width, var(--ui-badge-border-width))
    );
    border-style: var(
      --_border-style-resolved,
      var(--_badge-border-style, var(--ui-badge-border-style))
    );
    border-color: var(
      --_border-color-resolved,
      var(--_badge-border-color, var(--ui-badge-border-color))
    );
    border-radius: var(--_rounded-resolved, var(--_badge-rounded, var(--ui-badge-rounded)));

    color: var(--_text-resolved, var(--_badge-text, var(--ui-badge-text)));
    background-color: var(--_bg-resolved, var(--_badge-bg, var(--ui-badge-bg)));
    background-clip: padding-box;
    white-space-collapse: collapse;

    /* The ring is published to --_focus-shadow so the utilities layer's
       box-shadow emission keeps it above a --shadow or --ring utility; the ring
       widths grow from 0 on :focus-visible so the ring eases in. */
    --_ring-offset-width: 0px;
    --_ring-width: 0px;
    --_focus-in: max(0, -1 * sign(var(--_focus-ring-w, 1px)));
    --_ring-fill: color-mix(
      in oklch,
      var(--_ring-color-resolved, var(--_badge-ring-color, var(--ui-badge-ring-color)))
        var(--ring-opacity),
      transparent
    );
    --_focus-shadow:
      0 0 0 calc(var(--_ring-offset-width) * (1 - var(--_focus-in))) var(--ring-offset-shadow-color),
      0 0 0 calc((var(--_ring-offset-width) + var(--_ring-width)) * (1 - var(--_focus-in)))
        var(--_ring-fill, var(--ring-shadow-color)),
      inset 0 0 0 calc(var(--_ring-offset-width) * var(--_focus-in)) var(--ring-offset-shadow-color),
      inset 0 0 0 calc((var(--_ring-offset-width) + var(--_ring-width)) * var(--_focus-in))
        var(--_ring-fill, var(--ring-shadow-color));
    box-shadow:
      var(--_focus-shadow, 0 0 #0000),
      var(--_shadow-resolved, var(--_badge-shadow, var(--ui-badge-shadow)));

    cursor: default;
    text-decoration: none;

    /* transparent twin of the ring for forced-colors / high-contrast modes */
    outline: abs(var(--_focus-ring-w, var(--outline-width))) var(--outline-style) transparent;
    outline-offset: calc(
      var(--outline-offset) - var(--_focus-in) *
        (2 * var(--outline-offset) + abs(var(--_focus-ring-w, var(--outline-width))))
    );

    transition: var(--default-transition);

    user-select: none;
    text-box: trim-both ex alphabetic;
  }

  a[href] :where([data-ui~="badge"]):not(:disabled, [aria-disabled="true"]),
  a[href]:where([data-ui~="badge"]):not(:disabled, [aria-disabled="true"]) {
    cursor: pointer;
  }

  /* ===========================================================================
  BADGE STATES — interactive badges only (button, a[href])
  =========================================================================== */

  @media (hover: hover) {
    :where([data-ui~="badge"]):is(button, a[href]):hover:not(:disabled, [aria-disabled="true"]) {
      color: var(--_text-resolved, var(--_badge-text--hover, var(--ui-badge-text--hover)));
      background-color: var(--_bg-resolved, var(--_badge-bg--hover, var(--ui-badge-bg--hover)));
      border-color: var(
        --_border-color-resolved,
        var(--_badge-border-color--hover, var(--ui-badge-border-color--hover))
      );
    }
  }

  :where([data-ui~="badge"]):is(button, a[href]):active:not(:disabled, [aria-disabled="true"]) {
    color: var(--_text-resolved, var(--_badge-text--active, var(--ui-badge-text--active)));
    background-color: var(--_bg-resolved, var(--_badge-bg--active, var(--ui-badge-bg--active)));
    border-color: var(
      --_border-color-resolved,
      var(--_badge-border-color--active, var(--ui-badge-border-color--active))
    );
  }

  :where([data-ui~="badge"]):is(button, a[href]):focus-visible:not(
    :disabled,
    [aria-disabled="true"]
  ) {
    --_ring-offset-width: var(--ring-offset-width);
    --_ring-width: abs(var(--_focus-ring-w, var(--ring-width)));

    outline-color: transparent;
  }

  :where([data-ui~="badge"]):is(button, a[href])[aria-busy="true"] {
    cursor: progress;
  }

  /* ===========================================================================
  BADGE ICONS
  =========================================================================== */

  :where([data-ui~="badge"]) > svg {
    inline-size: var(--ui-badge-icon-size);
    block-size: var(--ui-badge-icon-size);
    pointer-events: none;
    justify-content: center;
    align-items: center;
    display: flex;
    padding: 0;
  }

  /* ===========================================================================
  BADGE VARIANTS — data-badge-variant writes the privates
  =========================================================================== */

  :where([data-ui~="badge"][data-badge-variant="primary"]) {
    --_badge-bg: var(--color-primary);
    --_badge-bg--hover: oklch(from var(--color-primary) l c h / 0.9);
    --_badge-bg--active: oklch(from var(--color-primary) l c h / 0.8);

    --_badge-text: var(--color-primary-foreground);
    --_badge-text--hover: var(--color-primary-foreground);
    --_badge-text--active: var(--color-primary-foreground);

    --_badge-border-color: transparent;
    --_badge-border-color--hover: transparent;
    --_badge-border-color--active: transparent;
  }

  :where([data-ui~="badge"][data-badge-variant="secondary"]) {
    --_badge-bg: var(--color-secondary);
    --_badge-bg--hover: oklch(from var(--color-secondary) l c h / 0.9);
    --_badge-bg--active: oklch(from var(--color-secondary) l c h / 0.8);

    --_badge-text: var(--color-secondary-foreground);
    --_badge-text--hover: var(--color-secondary-foreground);
    --_badge-text--active: var(--color-secondary-foreground);

    --_badge-border-color: transparent;
    --_badge-border-color--hover: transparent;
    --_badge-border-color--active: transparent;
  }

  :where([data-ui~="badge"][data-badge-variant="tertiary"]) {
    --_badge-bg: var(--color-tertiary);
    --_badge-bg--hover: oklch(from var(--color-tertiary) l c h / 0.9);
    --_badge-bg--active: oklch(from var(--color-tertiary) l c h / 0.8);

    --_badge-text: var(--color-tertiary-foreground);
    --_badge-text--hover: var(--color-tertiary-foreground);
    --_badge-text--active: var(--color-tertiary-foreground);

    --_badge-border-color: transparent;
    --_badge-border-color--hover: transparent;
    --_badge-border-color--active: transparent;
  }

  :where([data-ui~="badge"][data-badge-variant="muted"]) {
    --_badge-bg: var(--color-muted);
    --_badge-bg--hover: oklch(from var(--color-muted) l c h / calc(alpha - 0.02));
    --_badge-bg--active: var(--color-muted);

    --_badge-text: var(--color-foreground);
    --_badge-text--hover: var(--color-foreground);
    --_badge-text--active: var(--color-foreground);

    --_badge-border-color: transparent;
    --_badge-border-color--hover: transparent;
    --_badge-border-color--active: transparent;
  }

  :where([data-ui~="badge"][data-badge-variant="ghost"]) {
    --_badge-bg: transparent;
    --_badge-bg--hover: var(--color-muted);
    --_badge-bg--active: oklch(from var(--color-muted) l c h / calc(alpha - 0.02));

    --_badge-text: var(--color-foreground);
    --_badge-text--hover: var(--color-foreground);
    --_badge-text--active: var(--color-foreground);

    --_badge-border-color: transparent;
    --_badge-border-color--hover: transparent;
    --_badge-border-color--active: transparent;
  }

  /* Status variants (info, success, warning, destructive) share one soft
     design: the status color at 10% alpha (20% in dark) as the surface, and
     the status hue as the text with its lightness clamped (at most 0.45 in
     light, at least 0.82 in dark) so it holds 4.5:1 on the tint and on the
     hover and active steps; status-contrast.browser.test.ts checks it. No border. One rule derives the states from the
     surface; each variant names only its colors. */
  :where(
    [data-ui~="badge"]:is(
      [data-badge-variant="info"],
      [data-badge-variant="success"],
      [data-badge-variant="warning"],
      [data-badge-variant="destructive"]
    )
  ) {
    --_badge-bg--hover: oklch(from var(--_badge-bg) l c h / calc(alpha + 0.1));
    --_badge-bg--active: oklch(from var(--_badge-bg) l c h / calc(alpha + 0.15));

    --_badge-text--hover: var(--_badge-text);
    --_badge-text--active: var(--_badge-text);

    --_badge-border-color: transparent;
    --_badge-border-color--hover: transparent;
    --_badge-border-color--active: transparent;

    --_badge-shadow: 0 0 transparent;
  }

  :where([data-ui~="badge"][data-badge-variant="info"]) {
    --_badge-ring-color: var(--color-info);
    --_badge-bg: light-dark(
      oklch(from var(--color-info) l c h / 0.1),
      oklch(from var(--color-info) l c h / 0.2)
    );
    --_badge-text: light-dark(
      oklch(from var(--color-info) min(l, 0.45) c h),
      oklch(from var(--color-info) max(l, 0.82) c h)
    );
  }

  :where([data-ui~="badge"][data-badge-variant="success"]) {
    --_badge-ring-color: var(--color-success);
    --_badge-bg: light-dark(
      oklch(from var(--color-success) l c h / 0.1),
      oklch(from var(--color-success) l c h / 0.2)
    );
    --_badge-text: light-dark(
      oklch(from var(--color-success) min(l, 0.45) c h),
      oklch(from var(--color-success) max(l, 0.82) c h)
    );
  }

  :where([data-ui~="badge"][data-badge-variant="warning"]) {
    --_badge-ring-color: var(--color-warning);
    --_badge-bg: light-dark(
      oklch(from var(--color-warning) l c h / 0.1),
      oklch(from var(--color-warning) l c h / 0.2)
    );
    --_badge-text: light-dark(
      oklch(from var(--color-warning) min(l, 0.45) c h),
      oklch(from var(--color-warning) max(l, 0.82) c h)
    );
  }

  :where([data-ui~="badge"][data-badge-variant="destructive"]) {
    --_badge-ring-color: var(--color-destructive);
    --_badge-bg: light-dark(
      oklch(from var(--color-destructive) l c h / 0.1),
      oklch(from var(--color-destructive) l c h / 0.2)
    );
    --_badge-text: light-dark(
      oklch(from var(--color-destructive) min(l, 0.45) c h),
      oklch(from var(--color-destructive) max(l, 0.82) c h)
    );
  }

  :where([data-ui~="badge"][data-badge-variant="link"]) {
    --_badge-bg: transparent;
    --_badge-bg--hover: transparent;
    --_badge-bg--active: transparent;

    --_badge-text: var(--color-foreground);
    --_badge-text--hover: oklch(from var(--color-primary) l c h / 0.6);
    --_badge-text--active: oklch(from var(--color-primary) l c h / 0.5);

    --_badge-border-style: none;

    --_badge-px: 0;
    --_badge-rounded: 0;

    --_badge-shadow: 0 0 transparent;
  }

  /* ===========================================================================
  BADGE SIZES — data-badge-size
  =========================================================================== */

  :where([data-ui~="badge"][data-badge-size="icon"]) {
    inline-size: calc(var(--_h-len) + var(--_h-num) * var(--spacing));
    padding: 0;
    aspect-ratio: 1 / 1;
  }
}

Icon

<button type="button" data-ui="badge" data-badge-size="icon" aria-label="Icon badge default">
  <svg fill="currentColor" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256">
    <rect width="256" height="256" fill="none" />
    <circle cx="128" cy="128" r="12" />
    <circle cx="196" cy="128" r="12" />
    <circle cx="60" cy="128" r="12" />
  </svg>
</button>
<button
  type="button"
  data-ui="badge"
  data-badge-size="icon"
  data-badge-variant="primary"
  aria-label="Icon badge primary"
>
  <svg fill="currentColor" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256">
    <rect width="256" height="256" fill="none" />
    <circle cx="128" cy="128" r="12" />
    <circle cx="196" cy="128" r="12" />
    <circle cx="60" cy="128" r="12" />
  </svg>
</button>
<button
  type="button"
  data-ui="badge"
  data-badge-size="icon"
  data-badge-variant="muted"
  aria-label="Icon badge muted"
>
  <svg fill="currentColor" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256">
    <rect width="256" height="256" fill="none" />
    <circle cx="128" cy="128" r="12" />
    <circle cx="196" cy="128" r="12" />
    <circle cx="60" cy="128" r="12" />
  </svg>
</button>
<button
  type="button"
  data-ui="badge"
  data-badge-size="icon"
  data-badge-variant="ghost"
  aria-label="Icon badge ghost"
>
  <svg fill="currentColor" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256">
    <rect width="256" height="256" fill="none" />
    <circle cx="128" cy="128" r="12" />
    <circle cx="196" cy="128" r="12" />
    <circle cx="60" cy="128" r="12" />
  </svg>
</button>
/**
 * badge.css: Badge ([data-ui~="badge"])
 *
 * @layer      variables, components
 * @requires   _layers.css, _variables.css, _reset.css, _properties.css (--_*-resolved, --_focus-shadow), fields.css (--ui-field-* border/ring)
 * @uses       color-mix(), oklch(from …): variant hover/active tints
 * @uses       text-box: vertical trim (patchy browser support)
 * @tokens     --ui-badge-* (@layer variables; hooks are named after the utilities they back)
 * @consumedby combobox.css (selection tags)
 *
 * Every utility-backed declaration reads one chain, highest first:
 *   var(--_<utility>-resolved, var(--_badge-<utility>, var(--ui-badge-<utility>)))
 * States apply only to interactive badges (button, a[href]).
 */

@layer variables {
  :root {
    /* border + ring follow --ui-field-*; metrics stay independent (badges are smaller) */
    --ui-badge-ring-color: var(--ui-field-ring-color);

    --ui-badge-font-family: var(--font-family-body);
    --ui-badge-font-size: var(--font-size-xs);
    --ui-badge-font-weight: var(--font-weight-strong);
    --ui-badge-leading: 1;

    --ui-badge-bg: var(--color-background);
    --ui-badge-bg--hover: var(--color-muted);
    --ui-badge-bg--active: oklch(from var(--color-muted) l c h / calc(alpha - 0.02));

    --ui-badge-text: var(--color-foreground);
    --ui-badge-text--hover: var(--color-foreground);
    --ui-badge-text--active: var(--color-foreground);

    --ui-badge-border-width: var(--ui-field-border-width);
    --ui-badge-border-style: var(--ui-field-border-style);
    --ui-badge-border-color: var(--ui-field-border-color);
    --ui-badge-border-color--hover: var(--ui-field-border-color);
    --ui-badge-border-color--active: var(--ui-field-border-color);

    /* dual-mode: a scale number or a length */
    --ui-badge-gap: var(--spacing);
    --ui-badge-px: calc(var(--spacing) * 1.25);
    --ui-badge-h: calc(var(--spacing) * 5);
    --ui-badge-rounded: var(--radius-sm);

    --ui-badge-icon-size: calc(var(--spacing) * 3);

    /* never `none`: the focus ring layers into this box-shadow list */
    --ui-badge-shadow: 0 0 transparent;
  }
}

/* Variant privates: non-inheriting so a variant never cascades into a nested badge. */
@property --_badge-ring-color {
  syntax: "*";
  inherits: false;
}

@property --_badge-font-family {
  syntax: "*";
  inherits: false;
}

@property --_badge-font-size {
  syntax: "*";
  inherits: false;
}

@property --_badge-font-weight {
  syntax: "*";
  inherits: false;
}

@property --_badge-leading {
  syntax: "*";
  inherits: false;
}

@property --_badge-bg {
  syntax: "*";
  inherits: false;
}

@property --_badge-bg--hover {
  syntax: "*";
  inherits: false;
}

@property --_badge-bg--active {
  syntax: "*";
  inherits: false;
}

@property --_badge-text {
  syntax: "*";
  inherits: false;
}

@property --_badge-text--hover {
  syntax: "*";
  inherits: false;
}

@property --_badge-text--active {
  syntax: "*";
  inherits: false;
}

@property --_badge-border-width {
  syntax: "*";
  inherits: false;
}

@property --_badge-border-style {
  syntax: "*";
  inherits: false;
}

@property --_badge-border-color {
  syntax: "*";
  inherits: false;
}

@property --_badge-border-color--hover {
  syntax: "*";
  inherits: false;
}

@property --_badge-border-color--active {
  syntax: "*";
  inherits: false;
}

@property --_badge-gap {
  syntax: "*";
  inherits: false;
}

@property --_badge-px {
  syntax: "*";
  inherits: false;
}

@property --_badge-h {
  syntax: "*";
  inherits: false;
}

@property --_badge-rounded {
  syntax: "*";
  inherits: false;
}

@property --_badge-icon-size {
  syntax: "*";
  inherits: false;
}

@property --_badge-shadow {
  syntax: "*";
  inherits: false;
}

@layer ui.components {
  :where([data-ui~="badge"]) {
    font-family: var(
      --_font-family-resolved,
      var(--_badge-font-family, var(--ui-badge-font-family))
    );
    font-size: var(--_font-size-resolved, var(--_badge-font-size, var(--ui-badge-font-size)));
    font-weight: var(
      --_font-weight-resolved,
      var(--_badge-font-weight, var(--ui-badge-font-weight))
    );
    line-height: var(--_leading-resolved, var(--_badge-leading, var(--ui-badge-leading)));

    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;

    --_gap-len: var(--_gap-resolved, var(--_badge-gap, var(--ui-badge-gap)));
    --_gap-num: var(--_gap-resolved, var(--_badge-gap, var(--ui-badge-gap)));
    gap: calc(var(--_gap-len) + var(--_gap-num) * var(--spacing));

    --_h-len: var(--_h-resolved, var(--_badge-h, var(--ui-badge-h)));
    --_h-num: var(--_h-resolved, var(--_badge-h, var(--ui-badge-h)));
    block-size: calc(var(--_h-len) + var(--_h-num) * var(--spacing));
    min-inline-size: max-content;

    --_px-len: var(--_px-resolved, var(--_badge-px, var(--ui-badge-px)));
    --_px-num: var(--_px-resolved, var(--_badge-px, var(--ui-badge-px)));
    padding-inline: calc(var(--_px-len) + var(--_px-num) * var(--spacing));

    border-width: var(
      --_border-width-resolved,
      var(--_badge-border-width, var(--ui-badge-border-width))
    );
    border-style: var(
      --_border-style-resolved,
      var(--_badge-border-style, var(--ui-badge-border-style))
    );
    border-color: var(
      --_border-color-resolved,
      var(--_badge-border-color, var(--ui-badge-border-color))
    );
    border-radius: var(--_rounded-resolved, var(--_badge-rounded, var(--ui-badge-rounded)));

    color: var(--_text-resolved, var(--_badge-text, var(--ui-badge-text)));
    background-color: var(--_bg-resolved, var(--_badge-bg, var(--ui-badge-bg)));
    background-clip: padding-box;
    white-space-collapse: collapse;

    /* The ring is published to --_focus-shadow so the utilities layer's
       box-shadow emission keeps it above a --shadow or --ring utility; the ring
       widths grow from 0 on :focus-visible so the ring eases in. */
    --_ring-offset-width: 0px;
    --_ring-width: 0px;
    --_focus-in: max(0, -1 * sign(var(--_focus-ring-w, 1px)));
    --_ring-fill: color-mix(
      in oklch,
      var(--_ring-color-resolved, var(--_badge-ring-color, var(--ui-badge-ring-color)))
        var(--ring-opacity),
      transparent
    );
    --_focus-shadow:
      0 0 0 calc(var(--_ring-offset-width) * (1 - var(--_focus-in))) var(--ring-offset-shadow-color),
      0 0 0 calc((var(--_ring-offset-width) + var(--_ring-width)) * (1 - var(--_focus-in)))
        var(--_ring-fill, var(--ring-shadow-color)),
      inset 0 0 0 calc(var(--_ring-offset-width) * var(--_focus-in)) var(--ring-offset-shadow-color),
      inset 0 0 0 calc((var(--_ring-offset-width) + var(--_ring-width)) * var(--_focus-in))
        var(--_ring-fill, var(--ring-shadow-color));
    box-shadow:
      var(--_focus-shadow, 0 0 #0000),
      var(--_shadow-resolved, var(--_badge-shadow, var(--ui-badge-shadow)));

    cursor: default;
    text-decoration: none;

    /* transparent twin of the ring for forced-colors / high-contrast modes */
    outline: abs(var(--_focus-ring-w, var(--outline-width))) var(--outline-style) transparent;
    outline-offset: calc(
      var(--outline-offset) - var(--_focus-in) *
        (2 * var(--outline-offset) + abs(var(--_focus-ring-w, var(--outline-width))))
    );

    transition: var(--default-transition);

    user-select: none;
    text-box: trim-both ex alphabetic;
  }

  a[href] :where([data-ui~="badge"]):not(:disabled, [aria-disabled="true"]),
  a[href]:where([data-ui~="badge"]):not(:disabled, [aria-disabled="true"]) {
    cursor: pointer;
  }

  /* ===========================================================================
  BADGE STATES — interactive badges only (button, a[href])
  =========================================================================== */

  @media (hover: hover) {
    :where([data-ui~="badge"]):is(button, a[href]):hover:not(:disabled, [aria-disabled="true"]) {
      color: var(--_text-resolved, var(--_badge-text--hover, var(--ui-badge-text--hover)));
      background-color: var(--_bg-resolved, var(--_badge-bg--hover, var(--ui-badge-bg--hover)));
      border-color: var(
        --_border-color-resolved,
        var(--_badge-border-color--hover, var(--ui-badge-border-color--hover))
      );
    }
  }

  :where([data-ui~="badge"]):is(button, a[href]):active:not(:disabled, [aria-disabled="true"]) {
    color: var(--_text-resolved, var(--_badge-text--active, var(--ui-badge-text--active)));
    background-color: var(--_bg-resolved, var(--_badge-bg--active, var(--ui-badge-bg--active)));
    border-color: var(
      --_border-color-resolved,
      var(--_badge-border-color--active, var(--ui-badge-border-color--active))
    );
  }

  :where([data-ui~="badge"]):is(button, a[href]):focus-visible:not(
    :disabled,
    [aria-disabled="true"]
  ) {
    --_ring-offset-width: var(--ring-offset-width);
    --_ring-width: abs(var(--_focus-ring-w, var(--ring-width)));

    outline-color: transparent;
  }

  :where([data-ui~="badge"]):is(button, a[href])[aria-busy="true"] {
    cursor: progress;
  }

  /* ===========================================================================
  BADGE ICONS
  =========================================================================== */

  :where([data-ui~="badge"]) > svg {
    inline-size: var(--ui-badge-icon-size);
    block-size: var(--ui-badge-icon-size);
    pointer-events: none;
    justify-content: center;
    align-items: center;
    display: flex;
    padding: 0;
  }

  /* ===========================================================================
  BADGE VARIANTS — data-badge-variant writes the privates
  =========================================================================== */

  :where([data-ui~="badge"][data-badge-variant="primary"]) {
    --_badge-bg: var(--color-primary);
    --_badge-bg--hover: oklch(from var(--color-primary) l c h / 0.9);
    --_badge-bg--active: oklch(from var(--color-primary) l c h / 0.8);

    --_badge-text: var(--color-primary-foreground);
    --_badge-text--hover: var(--color-primary-foreground);
    --_badge-text--active: var(--color-primary-foreground);

    --_badge-border-color: transparent;
    --_badge-border-color--hover: transparent;
    --_badge-border-color--active: transparent;
  }

  :where([data-ui~="badge"][data-badge-variant="secondary"]) {
    --_badge-bg: var(--color-secondary);
    --_badge-bg--hover: oklch(from var(--color-secondary) l c h / 0.9);
    --_badge-bg--active: oklch(from var(--color-secondary) l c h / 0.8);

    --_badge-text: var(--color-secondary-foreground);
    --_badge-text--hover: var(--color-secondary-foreground);
    --_badge-text--active: var(--color-secondary-foreground);

    --_badge-border-color: transparent;
    --_badge-border-color--hover: transparent;
    --_badge-border-color--active: transparent;
  }

  :where([data-ui~="badge"][data-badge-variant="tertiary"]) {
    --_badge-bg: var(--color-tertiary);
    --_badge-bg--hover: oklch(from var(--color-tertiary) l c h / 0.9);
    --_badge-bg--active: oklch(from var(--color-tertiary) l c h / 0.8);

    --_badge-text: var(--color-tertiary-foreground);
    --_badge-text--hover: var(--color-tertiary-foreground);
    --_badge-text--active: var(--color-tertiary-foreground);

    --_badge-border-color: transparent;
    --_badge-border-color--hover: transparent;
    --_badge-border-color--active: transparent;
  }

  :where([data-ui~="badge"][data-badge-variant="muted"]) {
    --_badge-bg: var(--color-muted);
    --_badge-bg--hover: oklch(from var(--color-muted) l c h / calc(alpha - 0.02));
    --_badge-bg--active: var(--color-muted);

    --_badge-text: var(--color-foreground);
    --_badge-text--hover: var(--color-foreground);
    --_badge-text--active: var(--color-foreground);

    --_badge-border-color: transparent;
    --_badge-border-color--hover: transparent;
    --_badge-border-color--active: transparent;
  }

  :where([data-ui~="badge"][data-badge-variant="ghost"]) {
    --_badge-bg: transparent;
    --_badge-bg--hover: var(--color-muted);
    --_badge-bg--active: oklch(from var(--color-muted) l c h / calc(alpha - 0.02));

    --_badge-text: var(--color-foreground);
    --_badge-text--hover: var(--color-foreground);
    --_badge-text--active: var(--color-foreground);

    --_badge-border-color: transparent;
    --_badge-border-color--hover: transparent;
    --_badge-border-color--active: transparent;
  }

  /* Status variants (info, success, warning, destructive) share one soft
     design: the status color at 10% alpha (20% in dark) as the surface, and
     the status hue as the text with its lightness clamped (at most 0.45 in
     light, at least 0.82 in dark) so it holds 4.5:1 on the tint and on the
     hover and active steps; status-contrast.browser.test.ts checks it. No border. One rule derives the states from the
     surface; each variant names only its colors. */
  :where(
    [data-ui~="badge"]:is(
      [data-badge-variant="info"],
      [data-badge-variant="success"],
      [data-badge-variant="warning"],
      [data-badge-variant="destructive"]
    )
  ) {
    --_badge-bg--hover: oklch(from var(--_badge-bg) l c h / calc(alpha + 0.1));
    --_badge-bg--active: oklch(from var(--_badge-bg) l c h / calc(alpha + 0.15));

    --_badge-text--hover: var(--_badge-text);
    --_badge-text--active: var(--_badge-text);

    --_badge-border-color: transparent;
    --_badge-border-color--hover: transparent;
    --_badge-border-color--active: transparent;

    --_badge-shadow: 0 0 transparent;
  }

  :where([data-ui~="badge"][data-badge-variant="info"]) {
    --_badge-ring-color: var(--color-info);
    --_badge-bg: light-dark(
      oklch(from var(--color-info) l c h / 0.1),
      oklch(from var(--color-info) l c h / 0.2)
    );
    --_badge-text: light-dark(
      oklch(from var(--color-info) min(l, 0.45) c h),
      oklch(from var(--color-info) max(l, 0.82) c h)
    );
  }

  :where([data-ui~="badge"][data-badge-variant="success"]) {
    --_badge-ring-color: var(--color-success);
    --_badge-bg: light-dark(
      oklch(from var(--color-success) l c h / 0.1),
      oklch(from var(--color-success) l c h / 0.2)
    );
    --_badge-text: light-dark(
      oklch(from var(--color-success) min(l, 0.45) c h),
      oklch(from var(--color-success) max(l, 0.82) c h)
    );
  }

  :where([data-ui~="badge"][data-badge-variant="warning"]) {
    --_badge-ring-color: var(--color-warning);
    --_badge-bg: light-dark(
      oklch(from var(--color-warning) l c h / 0.1),
      oklch(from var(--color-warning) l c h / 0.2)
    );
    --_badge-text: light-dark(
      oklch(from var(--color-warning) min(l, 0.45) c h),
      oklch(from var(--color-warning) max(l, 0.82) c h)
    );
  }

  :where([data-ui~="badge"][data-badge-variant="destructive"]) {
    --_badge-ring-color: var(--color-destructive);
    --_badge-bg: light-dark(
      oklch(from var(--color-destructive) l c h / 0.1),
      oklch(from var(--color-destructive) l c h / 0.2)
    );
    --_badge-text: light-dark(
      oklch(from var(--color-destructive) min(l, 0.45) c h),
      oklch(from var(--color-destructive) max(l, 0.82) c h)
    );
  }

  :where([data-ui~="badge"][data-badge-variant="link"]) {
    --_badge-bg: transparent;
    --_badge-bg--hover: transparent;
    --_badge-bg--active: transparent;

    --_badge-text: var(--color-foreground);
    --_badge-text--hover: oklch(from var(--color-primary) l c h / 0.6);
    --_badge-text--active: oklch(from var(--color-primary) l c h / 0.5);

    --_badge-border-style: none;

    --_badge-px: 0;
    --_badge-rounded: 0;

    --_badge-shadow: 0 0 transparent;
  }

  /* ===========================================================================
  BADGE SIZES — data-badge-size
  =========================================================================== */

  :where([data-ui~="badge"][data-badge-size="icon"]) {
    inline-size: calc(var(--_h-len) + var(--_h-num) * var(--spacing));
    padding: 0;
    aspect-ratio: 1 / 1;
  }
}

Attributes

badge attributes
AttributeKindValuesNotes
data-ui="badge"Identityfree valueIdentity token in data-ui.
data-badge-sizePreseticonPreset: a named bundle of values.
data-badge-variantPresetdestructive ghost info link muted primary secondary success tertiary warningPreset: a named bundle of values.

Requires: fields.

Hooks

Set a hook on :root, on a subtree, or inline on one element. A state hook (--…--hover) themes that state only; the primitive keeps its other states.

badge hooks
HookDefaultState
--ui-badge-bgvar(--color-background)—
--ui-badge-bg--activeoklch(from var(--color-muted) l c h / calc(alpha - 0.02))active
--ui-badge-bg--hovervar(--color-muted)hover
--ui-badge-border-colorvar(--ui-field-border-color)—
--ui-badge-border-color--activevar(--ui-field-border-color)active
--ui-badge-border-color--hovervar(--ui-field-border-color)hover
--ui-badge-border-stylevar(--ui-field-border-style)—
--ui-badge-border-widthvar(--ui-field-border-width)—
--ui-badge-font-familyvar(--font-family-body)—
--ui-badge-font-sizevar(--font-size-xs)—
--ui-badge-font-weightvar(--font-weight-strong)—
--ui-badge-gapvar(--spacing)—
--ui-badge-hcalc(var(--spacing) * 5)—
--ui-badge-icon-sizecalc(var(--spacing) * 3)—
--ui-badge-leading1—
--ui-badge-pxcalc(var(--spacing) * 1.25)—
--ui-badge-ring-colorvar(--ui-field-ring-color)—
--ui-badge-roundedvar(--radius-sm)—
--ui-badge-shadow0 0 transparent—
--ui-badge-textvar(--color-foreground)—
--ui-badge-text--activevar(--color-foreground)active
--ui-badge-text--hovervar(--color-foreground)hover