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 sourceFigma
Interface design in the browser
FreemiumBerkeley 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
| Prop | Type | Default | Description |
|---|---|---|---|
| variant | "free" | "open-source" | "freemium" | "paid" | "new" | On Tag. Picks the style and the default label. | |
| label | string | On Tag. Overrides the default label. | |
| items | BadgeVariant[] | 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