For the complete documentation index, see llms.txt. Markdown variants are available by appending .md to documentation URLs. Homepage markdown is available at /llms.md/content.md. An agent skill is available at /.well-known/agent-skills/site-skill.md.

Badge

The Badge component from this design system, built on Base UI.

Installation

$ pnpm dlx shadcn@latest add @cobalt/badge

Options

  • variant="label" is the 14px label chip; subtle is the same with 4px more height; caption is the 12px version.
  • shape="pill" makes any variant fully rounded.
  • tabular uses equal-width digits, for counts and amounts that change.
  • size="xs" is a tighter 6px badge (px-1.5 py-px) for the counter, label, subtle and caption variants.

Source

import { mergeProps } from "@base-ui/react/merge-props";
import { useRender } from "@base-ui/react/use-render";
import { cva } from "class-variance-authority";
import type { VariantProps } from "class-variance-authority";
import { cn } from "cn";

const badgeVariants = cva(
  "group/badge inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3!",
  {
    defaultVariants: {
      variant: "default",
    },
    variants: {
      variant: {
        counter:
          "h-auto gap-0 rounded-[4px] border-0 px-1 py-px text-xs leading-4 font-semibold tracking-normal transition-none",
        label:
          "h-auto gap-0 rounded-[6px] border-0 px-1.5 py-0.5 text-sm leading-5 font-medium tracking-normal transition-[padding,font-size] duration-200 ease-[ease] motion-reduce:transition-none",
        subtle:
          "h-auto gap-0 rounded-[6px] border-0 px-1.5 py-1 text-sm leading-5 font-medium tracking-normal transition-[padding,font-size] duration-200 ease-[ease] motion-reduce:transition-none",
        caption:
          "h-auto gap-0 rounded-[6px] border-0 px-1.5 py-1 text-xs leading-4 font-medium tracking-[0.15px] transition-[padding,font-size] duration-200 ease-[ease] motion-reduce:transition-none",
        default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
        destructive:
          "bg-destructive/10 text-destructive focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:focus-visible:ring-destructive/40 [a]:hover:bg-destructive/20",
        ghost:
          "hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50",
        link: "text-primary underline-offset-4 hover:underline",
        outline:
          "border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground",
        secondary:
          "bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80",
      },
    },
  }
);

const badgeTones = {
  primary:
    "bg-[var(--badge-primary-background,var(--primary))] text-[color:var(--badge-primary-foreground,var(--primary-foreground))]",
  destructive:
    "bg-[var(--badge-destructive-background,var(--destructive))] text-[color:var(--badge-destructive-foreground,var(--destructive-foreground,var(--primary-foreground)))]",
  neutral:
    "bg-[var(--badge-neutral-background,var(--surface-strong,var(--muted)))] text-[color:var(--foreground-secondary,var(--muted-foreground))]",
  gray: "bg-[var(--surface-subtle,var(--muted))] text-foreground",
  soft: "bg-[var(--surface-subtle,var(--muted))] text-[color:var(--foreground-secondary,var(--muted-foreground))]",
  orange:
    "bg-[var(--badge-orange-background,var(--muted))] text-[color:var(--badge-orange-foreground,var(--foreground))]",
  lime: "bg-[var(--badge-lime-background,var(--muted))] text-[color:var(--badge-lime-foreground,var(--foreground))]",
  rose: "bg-[var(--badge-rose-background,var(--muted))] text-[color:var(--badge-rose-foreground,var(--foreground))]",
  yellow:
    "bg-[var(--badge-yellow-background,var(--muted))] text-[color:var(--badge-yellow-foreground,var(--foreground))]",
  cyan: "bg-[var(--badge-cyan-background,var(--muted))] text-[color:var(--badge-cyan-foreground,var(--foreground))]",
  blue: "bg-[var(--badge-blue-background,var(--muted))] text-[color:var(--badge-blue-foreground,var(--foreground))]",
  purple:
    "bg-[var(--badge-purple-background,var(--muted))] text-[color:var(--badge-purple-foreground,var(--foreground))]",
} as const;

function Badge({
  className,
  variant = "default",
  render,
  tone,
  shape = "default",
  size = "default",
  tabular = false,
  ...props
}: useRender.ComponentProps<"span"> &
  VariantProps<typeof badgeVariants> & {
    tone?: keyof typeof badgeTones;
    /**
     * `pill` makes any variant fully rounded. `circle` is a 20px round count
     * (e.g. on a bell) that grows into a pill for longer numbers.
     */
    shape?: "default" | "pill" | "circle";
    /** `xs` is a tighter 6px badge for the counter, label, subtle and caption variants. */
    size?: "default" | "xs";
    /** Equal-width digits, for counts and amounts that change. */
    tabular?: boolean;
  }) {
  const quiet = ["counter", "label", "subtle", "caption"].includes(
    variant ?? ""
  );
  return useRender({
    defaultTagName: "span",
    props: mergeProps<"span">(
      {
        className: cn(
          badgeVariants({ variant }),
          quiet && badgeTones[tone ?? "neutral"],
          quiet && size === "xs" && "rounded-[6px] px-1.5 py-px",
          shape === "pill" && "rounded-full",
          shape === "circle" &&
            "h-5 min-w-5 justify-center rounded-full px-1 py-0 leading-5 tabular-nums",
          tabular && "tabular-nums",
          className
        ),
      },
      props
    ),
    render,
    state: {
      slot: "badge",
      variant,
    },
  });
}

export { Badge, badgeVariants };