Styled inline badge with variant support, built as a themed span.

Preview
Style
Variants
DefaultSecondaryDestructiveOutlineGhostLink
Icon Left
Default Secondary Destructive Outline Ghost Link
Icon Right
Default Secondary Destructive Outline Ghost Link
With Spinner
Default Secondary Destructive Outline Ghost Link
Long Text
A badge with a lot of text to see how it wraps
Custom Colors
BlueGreenSkyPurpleBlueGreenSkyPurpleRed
src/demo/views/badge.ts241 lines
import type { Html, HtmlBuilder } from 'foldkit/html'

import { badge } from '../../generated/registry/ui/badge'
import { icon } from '../../generated/registry/lib/icons'
import { spinner } from '../../generated/registry/ui/spinner'
import { BadgeCheck, ArrowRight, ArrowUpRight } from 'lucide'

import { defineSlice } from '../slice'
import type { Model, Message } from '../assemble'

export const badgeView = (_model: Model, h: HtmlBuilder<Message>): Html =>
  h.div(
    [h.Class('flex w-full flex-col gap-8')],
    [
      h.div(
        [h.Class('flex w-full flex-col gap-2')],
        [
          h.div([h.Class('px-1 text-xs font-medium text-muted-foreground')], ['Variants']),
          h.div(
            [h.Class('flex flex-wrap gap-2')],
            [
              badge<Message>({}, ['Default'], h),
              badge<Message>({ variant: 'secondary' }, ['Secondary'], h),
              badge<Message>({ variant: 'destructive' }, ['Destructive'], h),
              badge<Message>({ variant: 'outline' }, ['Outline'], h),
              badge<Message>({ variant: 'ghost' }, ['Ghost'], h),
              badge<Message>({ variant: 'link' }, ['Link'], h),
            ],
          ),
        ],
      ),
      h.div(
        [h.Class('flex w-full flex-col gap-2')],
        [
          h.div([h.Class('px-1 text-xs font-medium text-muted-foreground')], ['Icon Left']),
          h.div(
            [h.Class('flex flex-wrap gap-2')],
            [
              badge<Message>({}, [icon(h, BadgeCheck, 'size-3'), ' Default'], h),
              badge<Message>(
                { variant: 'secondary' },
                [icon(h, BadgeCheck, 'size-3'), ' Secondary'],
                h,
              ),
              badge<Message>(
                { variant: 'destructive' },
                [icon(h, BadgeCheck, 'size-3'), ' Destructive'],
                h,
              ),
              badge<Message>(
                { variant: 'outline' },
                [icon(h, BadgeCheck, 'size-3'), ' Outline'],
                h,
              ),
              badge<Message>({ variant: 'ghost' }, [icon(h, BadgeCheck, 'size-3'), ' Ghost'], h),
              badge<Message>({ variant: 'link' }, [icon(h, BadgeCheck, 'size-3'), ' Link'], h),
            ],
          ),
        ],
      ),
      h.div(
        [h.Class('flex w-full flex-col gap-2')],
        [
          h.div([h.Class('px-1 text-xs font-medium text-muted-foreground')], ['Icon Right']),
          h.div(
            [h.Class('flex flex-wrap gap-2')],
            [
              badge<Message>({}, ['Default ', icon(h, ArrowRight, 'size-3')], h),
              badge<Message>(
                { variant: 'secondary' },
                ['Secondary ', icon(h, ArrowRight, 'size-3')],
                h,
              ),
              badge<Message>(
                { variant: 'destructive' },
                ['Destructive ', icon(h, ArrowRight, 'size-3')],
                h,
              ),
              badge<Message>(
                { variant: 'outline' },
                ['Outline ', icon(h, ArrowRight, 'size-3')],
                h,
              ),
              badge<Message>({ variant: 'ghost' }, ['Ghost ', icon(h, ArrowRight, 'size-3')], h),
              badge<Message>({ variant: 'link' }, ['Link ', icon(h, ArrowRight, 'size-3')], h),
            ],
          ),
        ],
      ),
      h.div(
        [h.Class('flex w-full flex-col gap-2')],
        [
          h.div([h.Class('px-1 text-xs font-medium text-muted-foreground')], ['With Spinner']),
          h.div(
            [h.Class('flex flex-wrap gap-2')],
            [
              badge<Message>({}, [spinner<Message>({}, h), ' Default'], h),
              badge<Message>({ variant: 'secondary' }, [spinner<Message>({}, h), ' Secondary'], h),
              badge<Message>(
                { variant: 'destructive' },
                [spinner<Message>({}, h), ' Destructive'],
                h,
              ),
              badge<Message>({ variant: 'outline' }, [spinner<Message>({}, h), ' Outline'], h),
              badge<Message>({ variant: 'ghost' }, [spinner<Message>({}, h), ' Ghost'], h),
              badge<Message>({ variant: 'link' }, [spinner<Message>({}, h), ' Link'], h),
            ],
          ),
        ],
      ),
      h.div(
        [h.Class('flex w-full flex-col gap-2')],
        [
          h.div([h.Class('px-1 text-xs font-medium text-muted-foreground')], ['asChild']),
          h.div(
            [h.Class('flex flex-wrap gap-2')],
            [
              h.a(
                [h.Href('#'), h.Class('inline-flex')],
                [badge<Message>({}, ['Link ', icon(h, ArrowUpRight, 'size-3')], h)],
              ),
              h.a(
                [h.Href('#'), h.Class('inline-flex')],
                [
                  badge<Message>(
                    { variant: 'secondary' },
                    ['Link ', icon(h, ArrowUpRight, 'size-3')],
                    h,
                  ),
                ],
              ),
              h.a(
                [h.Href('#'), h.Class('inline-flex')],
                [
                  badge<Message>(
                    { variant: 'destructive' },
                    ['Link ', icon(h, ArrowUpRight, 'size-3')],
                    h,
                  ),
                ],
              ),
              h.a(
                [h.Href('#'), h.Class('inline-flex')],
                [
                  badge<Message>(
                    { variant: 'ghost' },
                    ['Link ', icon(h, ArrowUpRight, 'size-3')],
                    h,
                  ),
                ],
              ),
            ],
          ),
        ],
      ),
      h.div(
        [h.Class('flex w-full flex-col gap-2')],
        [
          h.div([h.Class('px-1 text-xs font-medium text-muted-foreground')], ['Long Text']),
          h.div(
            [h.Class('flex flex-wrap gap-2')],
            [
              badge<Message>(
                { variant: 'secondary' },
                ['A badge with a lot of text to see how it wraps'],
                h,
              ),
            ],
          ),
        ],
      ),
      h.div(
        [h.Class('flex w-full flex-col gap-2')],
        [
          h.div([h.Class('px-1 text-xs font-medium text-muted-foreground')], ['Custom Colors']),
          h.div(
            [h.Class('flex flex-wrap gap-2')],
            [
              badge<Message>(
                { className: 'bg-blue-600 text-blue-50 dark:bg-blue-600 dark:text-blue-50' },
                ['Blue'],
                h,
              ),
              badge<Message>(
                { className: 'bg-green-600 text-green-50 dark:bg-green-600 dark:text-green-50' },
                ['Green'],
                h,
              ),
              badge<Message>(
                { className: 'bg-sky-600 text-sky-50 dark:bg-sky-600 dark:text-sky-50' },
                ['Sky'],
                h,
              ),
              badge<Message>(
                {
                  className: 'bg-purple-600 text-purple-50 dark:bg-purple-600 dark:text-purple-50',
                },
                ['Purple'],
                h,
              ),
              badge<Message>(
                { className: 'bg-blue-50 text-blue-700 dark:bg-blue-950 dark:text-blue-300' },
                ['Blue'],
                h,
              ),
              badge<Message>(
                { className: 'bg-green-50 text-green-700 dark:bg-green-950 dark:text-green-300' },
                ['Green'],
                h,
              ),
              badge<Message>(
                { className: 'bg-sky-50 text-sky-700 dark:bg-sky-950 dark:text-sky-300' },
                ['Sky'],
                h,
              ),
              badge<Message>(
                {
                  className: 'bg-purple-50 text-purple-700 dark:bg-purple-950 dark:text-purple-300',
                },
                ['Purple'],
                h,
              ),
              badge<Message>(
                { className: 'bg-red-50 text-red-700 dark:bg-red-950 dark:text-red-300' },
                ['Red'],
                h,
              ),
            ],
          ),
        ],
      ),
    ],
  )

export const slice = defineSlice({
  fields: {},
  init: {},
  messages: [],
  handlers: () => ({}),
})

This code is shown verbatim — it’s the exact file used for this preview. Much of the surrounding wiring (slice, fields, messages, handlers) belongs to this site’s demo harness; you only need the view and the state that matters for your own app. Adapt what you need.

Installation

Add this component to your project:

pnpm dlx shadcn@latest add @foldcn/badge

Source

The component ships as plain source — no build step, no wrapper. Copy it and make it yours.

registry/default/ui/badge.ts49 lines
import type { Html, HtmlBuilder } from 'foldkit/html'

type Child = Html | string

import { cn } from '@/lib/utils'

/** Badge variant keys — keep in sync with `badgeVariants`. */
export const badgeVariantKeys = [
  'default',
  'secondary',
  'destructive',
  'outline',
  'ghost',
  'link',
] as const

export const badgeVariants: Record<BadgeVariant, string> = {
  default: 'bg-primary text-primary-foreground [a]:hover:bg-primary/80',
  secondary: 'bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80',
  destructive: 'bg-destructive/10 [a]:hover:bg-destructive/20 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 text-destructive dark:bg-destructive/20',
  outline: 'border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground',
  ghost: 'hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50',
  link: 'text-primary underline-offset-4 hover:underline',
}

export type BadgeVariant = (typeof badgeVariantKeys)[number]

export const badgeClass =
  'h-5 gap-1 rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium transition-all has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&>svg]:size-3! group/badge inline-flex w-fit shrink-0 items-center justify-center overflow-hidden whitespace-nowrap 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'

type StyleConfig = Readonly<{ className?: string; variant?: BadgeVariant }>

/** Styled badge built as a themed `<span>` (mirrors the shadcn v4 `badge.tsx`
 *  default element). For a link badge, render an `<a>` child and apply
 *  `badgeClass` via `cn` — foldcn has no Radix `Slot`. */
export const badge = <M>(
  config: StyleConfig,
  children: ReadonlyArray<Child>,
  h: HtmlBuilder<M>,
): Html =>
  h.span(
    [
      h.Class(cn(badgeClass, badgeVariants[config.variant ?? 'default'], config.className)),
      h.DataAttribute('slot', 'badge'),
      h.DataAttribute('variant', config.variant ?? 'default'),
    ],
    children,
  )