Badge

A label for the metadata a reader scans rather than reads. Eight hues with a low-contrast pair each, plus inverted and the house gradient, in three sizes with room for an icon.

A label for the metadata a reader scans rather than reads: status, plan tier, environment. One or two words in Title Case, and the hue does the sorting before the words are read at all.

GrayBluePurpleAmberRedPinkGreenTeal
The eight hues at full strength.

Install

bunx shadcn@latest add @vernostudio/badge

Or, if components.json has no registry namespace to resolve @vernostudio against, point the CLI straight at the file:

bunx shadcn@latest add https://verno-studio.vercel.app/r/badge.json

Contrast

A badge earns attention it did not pay for: color on a page of text always wins. contrast is how that attention gets spent deliberately, and it takes one value, "low", because only the quiet direction needs a name. Written plain, the label sits on a solid fill and belongs to the one badge in the row that has to be seen first. Add contrast="low" and the same hue drops to its 900 step on a 200 tint, which is what a table of twenty rows can carry without turning into a fruit stand.

QueuedBuildingPreviewCanceledErrorBetaReadyStaging
contrast="low" across all eight hues. The default choice for dense surfaces.
<Badge variant="green">Ready</Badge>
<Badge variant="green" contrast="low">
  Ready
</Badge>

A workable rule: one high-contrast badge per view, low everywhere else. And the hue carries the category, never the whole message. "Error" in red reads as an error; a bare red dot does not, so the words stay.

Inverted and verno

Two variants sit outside the hue pairs and come in one strength, so contrast leaves them alone. Inverted is the loudest thing this component can say: it swaps the page's own foreground and background, so it stays the exact negative of any theme it lands in. Verno is the house mark, a dusk gradient that runs from evening blue through deep navy into burnt orange, the last light of the day compressed into a pill. It is a signature, so it holds still in both modes.

InvertedVerno Studio
One strength each. Spend them rarely.

Sizes

The three heights land on the 4px grid: 20, 24 and 32. Type steps with them, down to an 11px floor for sm; smaller than that, a medium weight starts to smudge. Digits are tabular, so a count that ticks does not make the badge wobble.

SmallMediumLarge
sm, md and lg. The text steps with the height, so the small one stays legible.

Reach for sm inside table rows and lg only where the badge is the point of the surface, like a plan card. md is the default for everything between.

Icons

icon draws before the label and the badge does the sizing: 12, 14 and 16px across the three heights, so an icon drawn on a 16 grid drops in at any size. The slot is decorative and hidden from assistive tech, which is exactly why it cannot replace the label, only sharpen it.

Checks PassedReadyVerified
The same icon at each size. Icon plus text, never icon alone.
<Badge icon={<Check />} variant="green" contrast="low">
  Checks Passed
</Badge>

Tokens

Three steps per hue: the high-contrast fill, the low-contrast tint and the text that sits on it. They ship with the component rather than with the theme, so installing the badge does not quietly widen your palette past the one step per hue the theme publishes.

The tint pairs quote the color scale. The fills mostly do not: a scale tunes its solid step for surfaces with nothing on them, and a white label on five of the hues measured under WCAG AA there. Those fills are solved instead, each the lightest value at its hue that still holds the label at 4.5:1, and a test measures every pair so the numbers cannot quietly drift back.

--badge-amber
oklch(81.87% 0.1969 76.46)
--badge-amber-subtle
oklch(96.81% 0.0495 90.24)
--badge-amber-subtle-foreground
oklch(52.79% 0.1496 54.65)
--badge-blue
oklch(57.61% 0.2508 258.23)
--badge-blue-subtle
oklch(96.29% 0.0195 250.59)
--badge-blue-subtle-foreground
oklch(53.18% 0.2399 256.99)
--badge-gray
oklch(0.562 0 0)
--badge-gray-subtle
oklch(0.94 0 0)
--badge-gray-subtle-foreground
oklch(0.205 0 0)
--badge-green
oklch(54.59% 0.1616 147.27)
--badge-green-subtle
oklch(96.92% 0.037 147.15)
--badge-green-subtle-foreground
oklch(51.75% 0.1453 147.65)
--badge-pink
oklch(59.41% 0.238 1.01)
--badge-pink-subtle
oklch(95.71% 0.0321 353.14)
--badge-pink-subtle-foreground
oklch(53.5% 0.2058 2.84)
--badge-purple
oklch(55.5% 0.3008 306.12)
--badge-purple-subtle
oklch(96.73% 0.0228 309.8)
--badge-purple-subtle-foreground
oklch(47.18% 0.2579 304)
--badge-red
oklch(59.23% 0.2394 23.03)
--badge-red-subtle
oklch(95.41% 0.0299 14.25)
--badge-red-subtle-foreground
oklch(54.99% 0.232 25.29)
--badge-teal
oklch(55.05% 0.0982 181.95)
--badge-teal-subtle
oklch(97.06% 0.0347 180.66)
--badge-teal-subtle-foreground
oklch(52.08% 0.1251 182.93)

Inverted and verno bring no tokens of their own. Inverted borrows --gray-1000 and --background-100 from the theme; verno carries its three dusk constants inline, the way a logo carries its colors.

Source

One file, one dependency: cva decides the classes, your cn merges them. badgeVariants is exported on its own for the rare surface that needs the look without the element.

components/badge.tsx
import { cva } from "class-variance-authority";
import type { ReactNode } from "react";

import { cn } from "@/lib/utils";

export const badgeVariants = cva(
  "inline-flex shrink-0 items-center whitespace-nowrap rounded-full font-medium tabular-nums",
  {
    compoundVariants: [
      {
        className: "bg-badge-amber-subtle text-badge-amber-subtle-foreground",
        contrast: "low",
        variant: "amber",
      },
      {
        className: "bg-badge-blue-subtle text-badge-blue-subtle-foreground",
        contrast: "low",
        variant: "blue",
      },
      {
        className: "bg-badge-gray-subtle text-badge-gray-subtle-foreground",
        contrast: "low",
        variant: "gray",
      },
      {
        className: "bg-badge-green-subtle text-badge-green-subtle-foreground",
        contrast: "low",
        variant: "green",
      },
      {
        className: "bg-badge-pink-subtle text-badge-pink-subtle-foreground",
        contrast: "low",
        variant: "pink",
      },
      {
        className: "bg-badge-purple-subtle text-badge-purple-subtle-foreground",
        contrast: "low",
        variant: "purple",
      },
      {
        className: "bg-badge-red-subtle text-badge-red-subtle-foreground",
        contrast: "low",
        variant: "red",
      },
      {
        className: "bg-badge-teal-subtle text-badge-teal-subtle-foreground",
        contrast: "low",
        variant: "teal",
      },
    ],
    defaultVariants: {
      size: "md",
      variant: "gray",
    },
    variants: {
      contrast: {
        low: "",
      },
      size: {
        lg: "h-8 gap-1.5 px-3 text-sm [&_svg]:size-4",
        md: "h-6 gap-1 px-2.5 text-xs [&_svg]:size-3.5",
        sm: "h-5 gap-1 px-2 text-[0.6875rem] [&_svg]:size-3",
      },
      variant: {
        amber: "bg-badge-amber text-black",
        blue: "bg-badge-blue text-white",
        gray: "bg-badge-gray text-white",
        green: "bg-badge-green text-white",
        inverted: "bg-gray-1000 text-background-100",
        pink: "bg-badge-pink text-white",
        purple: "bg-badge-purple text-white",
        red: "bg-badge-red text-white",
        teal: "bg-badge-teal text-white",
        verno: "bg-linear-to-r from-[#0a5dab] via-[#124276] to-[#c25329] text-white",
      },
    },
  },
);

interface BadgeProps {
  readonly children: ReactNode;
  readonly className?: string;
  readonly contrast?: "low";
  readonly icon?: ReactNode;
  readonly size?: "sm" | "md" | "lg";
  readonly variant?:
    | "gray"
    | "blue"
    | "purple"
    | "amber"
    | "red"
    | "pink"
    | "green"
    | "teal"
    | "inverted"
    | "verno";
}

export const Badge = ({
  children,
  className,
  contrast,
  icon,
  size = "md",
  variant = "gray",
}: BadgeProps) => (
  <span className={cn(badgeVariants({ contrast, size, variant }), className)}>
    {icon ? (
      <span aria-hidden="true" className="inline-flex shrink-0 items-center justify-center">
        {icon}
      </span>
    ) : null}
    {children}
  </span>
);