shadimage
whiskeyjack
uibadgesource: ok · hit

Badge

Tone-tinted pill label on the semantic color scales, for status and classification at a glance.

npx shadcn add "https://whiskeyjack.net/r/badge.json"
free

Where it goes

stats

Interaction

animated-counter

Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.

Dependencies

Bundle weight · light
class-variance-authority

Registry dependencies

https://whiskeyjack.net/r/utils.json

Files1 · source cached 10/8/2026, 1:51:16 PM

components/ui/badge.tsx
import * as React from 'react'
import { cva, type VariantProps } from 'class-variance-authority'
import { cn } from '@/lib/utils'

const badgeVariants = cva(
  'inline-flex items-center gap-1 whitespace-nowrap rounded-full px-2.5 py-1 text-xs font-medium',
  {
    variants: {
      tone: {
        neutral: '',
        success: '',
        info: '',
        warning: '',
        error: '',
        orange: '',
        yellow: '',
      },
      strong: {
        true: '',
        false: '',
      },
    },
    /* The tone recipes live here because `strong` swaps the whole recipe:
       soft = -100 bg / -700 text (dark: 30% -900 mix / -400), strong = one
       step deeper on every channel, for the top tier of a classification
       scale (e.g. a blood-pressure crisis). */
    compoundVariants: [
      {
        tone: 'neutral',
        strong: false,
        className:
          'bg-[var(--color-surface-muted-light)] dark:bg-[var(--color-surface-muted-dark)] text-[var(--color-text-secondary-light)] dark:text-[var(--color-text-secondary-dark)]',
      },
      {
        tone: 'neutral',
        strong: true,
        className:
          'bg-[var(--color-warm-200)] text-[var(--color-text-primary-light)] dark:bg-[var(--color-neutral-700)] dark:text-[var(--color-text-primary-dark)]',
      },
      {
        tone: 'success',
        strong: false,
        className:
          'bg-[var(--color-success-100)] text-[var(--color-success-700)] dark:bg-[color-mix(in_srgb,var(--color-success-900)_30%,transparent)] dark:text-[var(--color-success-400)]',
      },
      {
        tone: 'success',
        strong: true,
        className:
          'bg-[var(--color-success-200)] text-[var(--color-success-800)] dark:bg-[color-mix(in_srgb,var(--color-success-900)_50%,transparent)] dark:text-[var(--color-success-300)]',
      },
      {
        tone: 'info',
        strong: false,
        className:
          'bg-[var(--color-info-100)] text-[var(--color-info-700)] dark:bg-[color-mix(in_srgb,var(--color-info-900)_30%,transparent)] dark:text-[var(--color-info-400)]',
      },
      {
        tone: 'info',
        strong: true,
        className:
          'bg-[var(--color-info-200)] text-[var(--color-info-800)] dark:bg-[color-mix(in_srgb,var(--color-info-900)_50%,transparent)] dark:text-[var(--color-info-300)]',
      },
      {
        tone: 'warning',
        strong: false,
        className:
          'bg-[var(--color-warning-100)] text-[var(--color-warning-700)] dark:bg-[color-mix(in_srgb,var(--color-warning-900)_30%,transparent)] dark:text-[var(--color-warning-400)]',
      },
      {
        tone: 'warning',
        strong: true,
        className:
          'bg-[var(--color-warning-200)] text-[var(--color-warning-800)] dark:bg-[color-mix(in_srgb,var(--color-warning-900)_50%,transparent)] dark:text-[var(--color-warning-300)]',
      },
      {
        tone: 'error',
        strong: false,
        className:
          'bg-[var(--color-error-100)] text-[var(--color-error-700)] dark:bg-[color-mix(in_srgb,var(--color-error-900)_30%,transparent)] dark:text-[var(--color-error-400)]',
      },
      {
        tone: 'error',
        strong: true,
        className:
          'bg-[var(--color-error-200)] text-[var(--color-error-800)] dark:bg-[color-mix(in_srgb,var(--color-error-900)_50%,transparent)] dark:text-[var(--color-error-300)]',
      },
      {
        tone: 'orange',
        strong: false,
        className:
          'bg-[var(--color-orange-100)] text-[var(--color-orange-700)] dark:bg-[color-mix(in_srgb,var(--color-orange-900)_30%,transparent)] dark:text-[var(--color-orange-400)]',
      },
      {
        tone: 'orange',
        strong: true,
        className:
          'bg-[var(--color-orange-200)] text-[var(--color-orange-800)] dark:bg-[color-mix(in_srgb,var(--color-orange-900)_50%,transparent)] dark:text-[var(--color-orange-300)]',
      },
      {
        tone: 'yellow',
        strong: false,
        className:
          'bg-[var(--color-yellow-100)] text-[var(--color-yellow-700)] dark:bg-[color-mix(in_srgb,var(--color-yellow-900)_30%,transparent)] dark:text-[var(--color-yellow-400)]',
      },
      {
        tone: 'yellow',
        strong: true,
        className:
          'bg-[var(--color-yellow-200)] text-[var(--color-yellow-800)] dark:bg-[color-mix(in_srgb,var(--color-yellow-900)_50%,transparent)] dark:text-[var(--color-yellow-300)]',
      },
    ],
    defaultVariants: {
      tone: 'neutral',
      strong: false,
    },
  },
)

export interface BadgeProps
  extends React.HTMLAttributes<HTMLSpanElement>,
    VariantProps<typeof badgeVariants> {}

/**
 * Tone-tinted pill label on the semantic color scales – the smallest member
 * of the tone family (Notice is the in-flow box, Toast the floating pill).
 * For classifications and statuses read at a glance: a blood-pressure
 * category, a roadmap state, a sync status. `strong` deepens the tint one
 * step for the top tier of a scale (crisis / very-high readings).
 *
 * Non-interactive and i18n-agnostic: pass translated text as children. The
 * tint never carries meaning alone – the label IS the information, the tone
 * reinforces it.
 */
const Badge = React.forwardRef<HTMLSpanElement, BadgeProps>(
  ({ tone, strong, className, ...props }, ref) => (
    <span ref={ref} className={cn(badgeVariants({ tone, strong }), className)} {...props} />
  ),
)
Badge.displayName = 'Badge'

export { Badge, badgeVariants }

More from whiskeyjack

Browse all components in this library →