Badge
A small status marker.
import { Div } from "@implementjs/core";
import { CircleCheckIcon } from "@implementjs/lucide";
import { Badge } from "@/lib/components/ui/badge";
export default function BadgeDemo() {
return Div(
{ class: "flex flex-wrap items-center justify-center gap-2" },
Badge("Default"),
Badge({ variant: "secondary" }, "Secondary"),
Badge({ variant: "outline" }, "Outline"),
Badge({ variant: "destructive" }, "Destructive"),
Badge(CircleCheckIcon({ "aria-hidden": true }), "Verified"),
Badge({ variant: "secondary", class: "rounded-full px-1.5 tabular-nums" }, "8"),
);
}Installation
npx jsrepo add @implementjs/ui/badge
It installs tailwind-variants at the same time.
Copy the file below to src/lib/components/ui/badge.ts. It imports cn, so copy utils.ts to src/lib/utils.ts too. Then, on top of @implementjs/core and @implementjs/primitives:
npm install tailwind-variants
import { Span, type Child, type ElementProps } from "@implementjs/core";
import { createComponent } from "@implementjs/primitives";
import { tv, type VariantProps } from "tailwind-variants";
import { cn } from "@/lib/utils";
export const badgeVariants = tv({
base: "inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-md border px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3",
variants: {
variant: {
default: "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
secondary:
"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
destructive:
"border-transparent bg-destructive text-white focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40 [a&]:hover:bg-destructive/90",
outline: "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
},
},
defaultVariants: { variant: "default" },
});
export type BadgeVariant = VariantProps<typeof badgeVariants>["variant"];
export type BadgeProps = ElementProps<"span"> & VariantProps<typeof badgeVariants>;
/**
* A small status marker. It renders a `Span`; for a badge that navigates,
* put `badgeVariants()` on an `A` instead — the hover rules are written
* against `[a&]`, so they only apply once the badge really is a link.
*/
export const Badge = createComponent(function Badge(
{ class: className, variant = "default", ...props }: BadgeProps,
...children: Child[]
) {
return Span(
{
...props,
"data-slot": "badge",
"data-variant": variant,
class: cn(badgeVariants({ variant }), className),
},
...children,
);
});Usage
import { Badge } from "@/lib/components/ui/badge";
Badge("Default");
Badge({ variant: "outline" }, "Draft");
Badge({ variant: "destructive" }, "Failed");
As a link
Badge renders a Span. For a badge that navigates, put badgeVariants() on an A instead:
import { badgeVariants } from "@/lib/components/ui/badge";
A({ href: "/releases", class: badgeVariants({ variant: "outline" }) }, "v2.1.0");
The hover rules are written against [a&], so they only switch on once the badge really is a link — a Span badge does not pretend to be clickable.
Counts
A round count is a class, not a variant:
Badge({ variant: "secondary", class: "rounded-full px-1.5 tabular-nums" }, "8");
tabular-nums keeps the badge from resizing as the number ticks.
API Reference
Badge
A status marker. `badgeVariants` is exported so a badge that navigates can be an `A` instead — the hover rules only apply once it really is a link. Renders a Span; extra props are forwarded onto it.
| Prop | Type | Default | Description |
|---|---|---|---|
variant | "default" | "secondary" | "destructive" | "outline" | "default" | Which badge style to render. Also set as data-variant. |