velobits
uibadgesource: ok · refreshed
Badge
Soft washes pair with the matching text token, never with the solid fill.
npx shadcn add "https://ui.velobits.dev/r/badge.json"free
Visual style
pastel
Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.
Dependencies
Bundle weight · lightclass-variance-authorityradix-ui
Registry dependencies
https://ui.velobits.dev/r/cn.json
Files1 · source cached 10/8/2026, 10:09:14 AM
registry/velobits/ui/badge.tsx
'use client';
import { cva, type VariantProps } from 'class-variance-authority';
import { Slot } from 'radix-ui';
import { cn } from '@/lib/utils';
/**
* The `*-soft` variants pair a tinted wash with the matching *text* token, never
* with the solid fill , `bg-success-soft text-success`, not
* `bg-success text-success`. Badge text is 12px, so the full 4.5:1 applies, and
* the colour the text actually sits on is the wash FLATTENED over the surface
* beneath the chip , so each pairing is gated as a composite over the page, the
* panel and the tier-S glass surface, in both themes: the soft-chip suite in
* `@velobitsio/tokens` (`test/contrast.test.ts`, driven by `SOFT_CHIP_PAIRS`).
* The token values were tuned to hold that gate.
*
* `brand` is the exception that proves the palette's asymmetry: lime is a fill
* with charcoal on it in both themes. There is no soft-lime-with-lime-text
* variant, because lime on cream is 1.13:1.
*/
const badgeVariants = cva(
[
'inline-flex w-fit shrink-0 items-center justify-center gap-1',
'rounded-md border px-2 py-0.5 text-xs font-medium whitespace-nowrap',
'transition-colors duration-micro ease-out',
"[&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-3",
],
{
variants: {
variant: {
neutral: 'border-border bg-bg2 text-fg',
primary: 'border-transparent bg-primary-soft text-link',
brand: 'border-transparent bg-brand text-on-brand',
success: 'border-transparent bg-success-soft text-success',
danger: 'border-transparent bg-danger-soft text-danger',
warning: 'border-transparent bg-warning-soft text-warning',
info: 'border-transparent bg-info-soft text-info',
/**
* A CATEGORY, not a status , the point of it.
*
* `success`/`danger`/`warning`/`info` all assert a severity, so an axis
* whose values are *kinds* (a flag's value type, a resource class, an
* environment that is neither production nor staging) had to borrow
* `primary` and came out blue. This is the variant to reach for there, and
* `neutral` is the one to reach for when the value genuinely has no
* category either.
*/
rose: 'border-transparent bg-rose-soft text-rose',
outline: 'border-field-border text-fg',
},
},
defaultVariants: { variant: 'neutral' },
},
);
export interface BadgeProps
extends React.ComponentProps<'span'>, VariantProps<typeof badgeVariants> {
asChild?: boolean;
}
function Badge({ className, variant, asChild = false, ...props }: BadgeProps) {
const Comp = asChild ? Slot.Root : 'span';
return (
<Comp data-slot="badge" className={cn(badgeVariants({ variant }), className)} {...props} />
);
}
export { Badge, badgeVariants };