Badge
ui
↳ lib-utils
Preview
Default
Secondary
Destructive
Outline
Ghost
Link
<div class="flex flex-wrap items-center gap-2">
<span class=${badgeClass()}>Default</span>
<span class=${badgeClass({ variant: 'secondary' })}>Secondary</span>
<span class=${badgeClass({ variant: 'destructive' })}>Destructive</span>
<span class=${badgeClass({ variant: 'outline' })}>Outline</span>
<span class=${badgeClass({ variant: 'ghost' })}>Ghost</span>
<span class=${badgeClass({ variant: 'link' })}>Link</span>
</div>Installation
webjs ui add badgeAPI Reference
Parts
| Name | Description |
|---|---|
badgeClass({ variant }) |
Apply to a span, link, or button. |
Props
| Prop | Type | Default | Description |
|---|---|---|---|
variant |
"default" | "secondary" | "destructive" | "outline" | "ghost" | "link" |
"default" | Visual style. The extra "ghost" and "link" variants are webjs additions beyond shadcn’s four. |
Source: components/ui/badge.ts
/**
* Badge: small visual label. Tier-1 class helper; compose with any
* inline element (commonly `<span>` or `<a>` for linked badges).
*
* shadcn parity:
* Badge (variant: default | secondary | destructive | outline | ghost | link)
* → badgeClass({ variant })
*
* The `[a&]:hover:...` hover styles only apply when the element is an `<a>`,
* so a static `<span>` doesn't pick up an unwanted hover.
*
* A11y (required for accessible output): render a static badge as a plain
* <span> (not focusable, no tabindex). Only an interactive badge (an <a>
* or <button>) is focusable, and an icon-only one needs an aria-label.
*
* Design tokens used: --primary, --secondary, --destructive, --foreground,
* --accent, --border, --ring.
*
* @example
* ```html
* <span class=${badgeClass()}>New</span>
* <span class=${badgeClass({ variant: 'destructive' })}>Error</span>
* <a class=${badgeClass({ variant: 'link' })} href="/profile">@vivek</a>
* ```
*/
import { cn } from '../lib/utils.ts';
const BASE =
'inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-full border border-transparent 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';
const VARIANTS = {
default: 'bg-primary text-primary-foreground [a&]:hover:bg-primary/90',
secondary: 'bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90',
destructive:
'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:
'border-border text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground',
ghost: '[a&]:hover:bg-accent [a&]:hover:text-accent-foreground',
link: 'text-primary underline-offset-4 [a&]:hover:underline',
} as const;
export type BadgeVariant = keyof typeof VARIANTS;
export function badgeClass(opts: { variant?: BadgeVariant } = {}): string {
return cn(BASE, VARIANTS[opts.variant ?? 'default']);
}