Components
ComponentsTier 3 · Components
Badge & Chip
Status and category labels, plus the removable chip.
bunx --bun shadcn@latest add @woohl/badgePreview
Reference
Installs this item, its registry dependencies and the token layer.
bunx --bun shadcn@latest add @woohl/badgeregistry/tier-3-components/badge.tsx
import type { ComponentProps, ReactElement } from "react";
import { cn } from "@/registry/tier-2-primitives/lib/utils";
import {
cva,
focusRing,
type VariantProps,
} from "@/registry/tier-2-primitives/lib/cva";
import { CloseGlyph } from "@/registry/tier-2-primitives/glyphs";
import { IconSlot } from "@/registry/tier-2-primitives/icon-slot";
/** Static label for status, category, or count. */
const badgeVariants = cva(
cn(
"inline-flex items-center gap-1.5 whitespace-nowrap rounded-pill",
"font-ui text-caption font-bold leading-[1.35]",
),
{
variants: {
tone: {
/** Default. Metadata that isn't a state. */
neutral: "bg-surface-sunken text-text-secondary",
/** Brand emphasis — "New in", "Curated". Uses the AA-safe accent text. */
accent: "bg-surface-accent-subtle text-text-accent-strong",
/** Order confirmed, payout sent, verified maker. */
success: "bg-success-subtle text-success",
/** Low stock, action needed, expiring soon. */
warning: "bg-warning-subtle text-warning",
/** Failed payment, cancelled, validation error. */
danger: "bg-danger-subtle text-danger",
/** Neutral system messages, tips, shipping updates. */
info: "bg-info-subtle text-info",
/** High contrast, for a badge sitting on a photograph. */
ink: "bg-surface-inverse text-text-inverse",
/** On a dark surface or an image, where a fill would be heavy. */
outline:
"border border-border-subtle bg-transparent text-text-secondary",
},
size: {
sm: "px-2 py-0.5 text-caption",
md: "px-2.5 py-[5px]",
},
},
defaultVariants: { tone: "neutral", size: "md" },
},
);
export type BadgeProps = ComponentProps<"span"> &
VariantProps<typeof badgeVariants> & {
/** Leading icon as a node: `icon={<Icon name="tag" />}`. */
icon?: ReactElement;
};
export function Badge({
className,
tone,
size,
icon,
children,
...props
}: BadgeProps) {
return (
<span
data-slot="badge"
className={cn(badgeVariants({ tone, size }), className)}
{...props}
>
{icon && <IconSlot icon={icon} size={12} />}
{children}
</span>
);
}
/** Actionable badge — a filter, tag, or selected facet. */
export type ChipProps = Omit<ComponentProps<"button">, "onSelect"> & {
selected?: boolean;
/** Renders a × that removes the chip. Gets its own accessible name. */
onRemove?: () => void;
removeLabel?: string;
/** Leading icon as a node: `icon={<Icon name="tag" />}`. */
icon?: ReactElement;
};
export function Chip({
className,
selected = false,
onRemove,
removeLabel,
icon,
children,
type = "button",
...props
}: ChipProps) {
return (
<span
data-slot="chip"
className={cn(
"rounded-pill inline-flex h-[var(--control-h-sm)] items-center gap-1.5 border px-3.5",
"font-ui text-label font-semibold",
"motion-instant transition-[background-color,border-color,color]",
selected
? "border-surface-inverse bg-surface-inverse text-text-inverse"
: "border-border-subtle bg-surface-raised text-text-secondary hover:border-border-strong",
className,
)}
>
<button
type={type}
aria-pressed={selected}
className={cn(
"rounded-pill inline-flex items-center gap-1.5",
focusRing,
)}
{...props}
>
{icon && <IconSlot icon={icon} size={13} />}
{children}
</button>
{onRemove && (
<button
type="button"
onClick={onRemove}
aria-label={
removeLabel ??
`Remove ${typeof children === "string" ? children : "filter"}`
}
className={cn(
"grid place-items-center rounded-full text-current opacity-70 hover:opacity-100",
focusRing,
)}
>
<CloseGlyph size={13} />
</button>
)}
</span>
);
}
export { badgeVariants };
BadgeProps
Also accepts everything from ComponentProps<"span">.
| Prop | Type | Default |
|---|---|---|
iconLeading icon as a node: `icon={<Icon name="tag" />}`. | ReactElement | — |
size | "sm" | "md" | "md" |
tone | "neutral" | "accent" | "success" | "warning" | "danger" | "info" | "ink" | "outline" | "neutral" |
ChipProps
Also accepts everything from Omit<ComponentProps<"button">, "onSelect">.
| Prop | Type | Default |
|---|---|---|
iconLeading icon as a node: `icon={<Icon name="tag" />}`. | ReactElement | — |
onRemoveRenders a × that removes the chip. Gets its own accessible name. | () => void | — |
removeLabel | string | — |
selected | boolean | — |
Registry items
npm packages
None beyond React itself.