Skip to content
goodtabs UI
BrowseBrowse components

Badge set

The pricing badges in one component: free, open source, freemium and paid, plus new.

npx shadcn@latest add https://ui.goodtabs.co/r/badge-set.json

With the @goodtabs namespace in components.json: npx shadcn@latest add @goodtabs/badge-set

  • Free
  • Open source
  • Freemium
  • Paid

All four pricing variants side by side.

In a list

  • Motion

    Animation library for React

    Open source
  • Figma

    Interface design in the browser

    Freemium
  • Berkeley Mono

    A monospaced typeface

    NewPaid

A badge at the end of each row, with the New dot before it.

Usage

import { BadgeSet, Tag } from "@/components/ui/badge-set"

<Tag variant="paid" />
<BadgeSet items={["free", "open-source"]} />

Props

PropTypeDefaultDescription
variant"free" | "open-source" | "freemium" | "paid" | "new"On Tag. Picks the style and the default label.
labelstringOn Tag. Overrides the default label.
itemsBadgeVariant[]On BadgeSet. Variants in display order.

Source

badge-set/badge-set.tsx
import * as React from "react";
import { cn } from "@/lib/utils";

export type BadgeVariant = "free" | "open-source" | "freemium" | "paid" | "new";

const LABEL: Record<BadgeVariant, string> = {
  free: "Free",
  "open-source": "Open source",
  freemium: "Freemium",
  paid: "Paid",
  new: "New",
};

const STYLE: Record<BadgeVariant, string> = {
  free: "border text-muted-foreground",
  "open-source": "border text-muted-foreground",
  freemium: "border border-transparent bg-muted text-foreground",
  paid: "border border-transparent bg-primary text-primary-foreground",
  new: "border text-muted-foreground",
};

type TagProps = Omit<React.ComponentProps<"span">, "children"> & {
  /** Picks the style and the default label. */
  variant: BadgeVariant;
  /** Overrides the default label. */
  label?: string;
};

/** One badge: rounded-sm, 20px tall, 12px text. Free and Open source are outlined, Freemium soft, Paid solid. */
export function Tag({ variant, label, className, ...props }: TagProps) {
  return (
    <span
      data-slot="badge"
      data-variant={variant}
      className={cn(
        "inline-flex h-5 items-center gap-1.5 rounded-sm px-1.5 text-xs font-medium whitespace-nowrap",
        STYLE[variant],
        className,
      )}
      {...props}
    >
      {variant === "new" && <span aria-hidden="true" className="size-1.5 rounded-full bg-brand" />}
      {label ?? LABEL[variant]}
    </span>
  );
}

type BadgeSetProps = Omit<React.ComponentProps<"ul">, "children"> & {
  /** Variants in display order. */
  items: BadgeVariant[];
};

/** A wrapping list of badges with 6px gaps. */
export function BadgeSet({ items, className, ...props }: BadgeSetProps) {
  return (
    <ul
      data-slot="badge-set"
      className={cn("m-0 flex list-none flex-wrap items-center gap-1.5 p-0", className)}
      {...props}
    >
      {items.map((variant) => (
        <li key={variant}>
          <Tag variant={variant} />
        </li>
      ))}
    </ul>
  );
}

For AI agents

Hand your agent this page, the markdown at /components/badge-set/prompt.md, or the Copy AI prompt button above.

When to use

  • Showing the price model of a resource at a glance
  • A small state label beside a title in a list

When not to use

  • Counts and numbers: use a plain mono label
  • Anything clickable: use a button or a filter chip

Agent notes

  • Server component. Tag is one badge, BadgeSet is a list of them
  • Free and Open source are outlined, Freemium is a soft fill with no border, Paid is the solid primary fill
  • New is outlined with a brand dot, the only coloured mark
  • Meaning never rests on colour alone: every badge carries its text