Components
Badge
A small visual marker — status pill, counter, label. Non-interactive by default; renders as <span>, switches to <a> when an href is supplied.
Installation
1. Install via the shadcn CLI
npx shadcn@latest add http://localhost/r/badge.json2. Use it
import { Badge } from "@/components/ui/badge"
<Badge>New</Badge>
<Badge variant="destructive">Unstable</Badge>
<Badge as="a" href="/docs">Docs</Badge>Or copy the source manually
/** @jsxImportSource hono/jsx */
import type { PropsWithChildren } from "hono/jsx"
import { cloneElement, isValidElement } from "hono/jsx"
import { cn, type ClassValue } from "@/registry/lib/cn"
// Badge — shadcn-htmx, htmx v4 + Tailwind v4.
//
// Source of truth (variants + base class 1:1):
// repos/shadcn-ui/apps/v4/registry/new-york-v4/ui/badge.tsx
//
// A non-interactive visual marker. Renders as <span> by default. Pass
// `as="a"` (or `asChild` with a child anchor) to make it a link — the
// upstream uses Radix Slot.Root; we use cloneElement.
//
// Accessibility:
// - The badge's content (text) IS the accessible name. If you render an
// icon-only badge, set `ariaLabel` so screen readers can name it.
// - Status-style badges ("New", "3 unread") that update in place should
// live inside an aria-live region (use Alert / Toast for that, not
// Badge). Badge itself is presentational.
// - See repos/mdn/files/en-us/web/html/reference/elements/span/index.md for
// <span> semantics (none — it's a generic inline container).
export type BadgeVariant =
| "default"
| "secondary"
| "destructive"
| "outline"
| "ghost"
| "link"
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: Record<BadgeVariant, string> = {
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",
}
export function badgeClasses(opts?: {
variant?: BadgeVariant
class?: ClassValue
}): string {
const variant = opts?.variant ?? "default"
return cn(base, variants[variant], opts?.class)
}
type BadgeProps = PropsWithChildren<{
variant?: BadgeVariant
class?: ClassValue
id?: string
ariaLabel?: string
ariaLabelledby?: string
// Render as a different element. <a> is the common case (link badge).
as?: "span" | "a" | "div" | "button"
href?: string
// SSR-friendly equivalent of shadcn's asChild — clone the single JSX
// child and merge classes onto it. Useful for wrapping a custom <Link>.
asChild?: boolean
}>
export function Badge(props: BadgeProps) {
const {
children,
variant,
class: className,
as,
href,
asChild,
ariaLabel,
ariaLabelledby,
id,
...rest
} = props
const classes = badgeClasses({ variant, class: className })
if (asChild && isValidElement(children)) {
const child = children as any
return cloneElement(child, {
...rest,
class: cn(classes, child?.props?.class),
"data-slot": "badge",
"data-variant": variant ?? "default",
"aria-label": ariaLabel,
"aria-labelledby": ariaLabelledby,
})
}
const Tag: any = as ?? (href ? "a" : "span")
return (
<Tag
id={id}
href={href}
data-slot="badge"
data-variant={variant ?? "default"}
class={classes}
aria-label={ariaLabel}
aria-labelledby={ariaLabelledby}
{...rest}
>
{children}
</Tag>
)
}
1. Save the file
Copy badge.html into templates/components/.
2. Use it
{% from "components/badge.html" import badge %}
{{ badge("New") }}
{{ badge("Unstable", variant="destructive") }}
{{ badge("Docs", tag="a", href="/docs") }}View source
{# Badge macro — shadcn-htmx, htmx v4 + Tailwind v4.
Mirrors registry/ui/badge.tsx. Renders <span> by default; pass tag="a"
+ href for a link badge, tag="button" for an interactive one.
Usage:
{% from "components/badge.html" import badge %}
{{ badge("New", variant="default") }}
{{ badge("Unstable", variant="destructive") }}
{{ badge("Docs", tag="a", href="/docs") }} #}
{% macro badge(
text,
variant="default",
tag="span",
href=none,
id=none,
aria_label=none,
aria_labelledby=none,
extra_class="",
**attrs
) %}
{%- set 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
{%- endset -%}
{%- set 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"
} -%}
<{{ tag }}
{%- if id %} id="{{ id }}"{% endif %}
{%- if href and tag == "a" %} href="{{ href }}"{% endif %}
{%- if aria_label %} aria-label="{{ aria_label }}"{% endif %}
{%- if aria_labelledby %} aria-labelledby="{{ aria_labelledby }}"{% endif %}
data-slot="badge"
data-variant="{{ variant }}"
class="{{ base }} {{ variants[variant] }} {{ extra_class }}"
{%- for k, v in attrs.items() %} {{ k|replace('_', '-') }}="{{ v }}"{% endfor -%}
>{{ text }}</{{ tag }}>
{% endmacro %}
1. Save the file
Add badge.tmpl alongside button.tmpl.
2. Use it
{{template "badge" (dict "Text" "New")}}
{{template "badge" (dict "Text" "Unstable" "Variant" "destructive")}}
{{template "badge" (dict "Text" "Docs" "Tag" "a" "Href" "/docs")}}View source
{{/*
Badge template — shadcn-htmx, htmx v4 + Tailwind v4.
Mirrors registry/ui/badge.tsx.
Usage:
type BadgeArgs struct {
Text string
Variant string // default | secondary | destructive | outline | ghost | link
Tag string // "span" (default) | "a" | "div" | "button"
Href string
ID, AriaLabel string
AriaLabelledby string
Attrs map[string]string
}
*/}}
{{define "badge"}}
{{- $tag := or .Tag "span" -}}
{{- $variant := or .Variant "default" -}}
{{- $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" -}}
{{- $variants := dict
"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" -}}
<{{$tag}}
{{- if .ID}} id="{{.ID}}"{{end}}
{{- if and (eq $tag "a") .Href}} href="{{.Href}}"{{end}}
{{- if .AriaLabel}} aria-label="{{.AriaLabel}}"{{end}}
{{- if .AriaLabelledby}} aria-labelledby="{{.AriaLabelledby}}"{{end}}
data-slot="badge" data-variant="{{$variant}}"
class="{{$base}} {{index $variants $variant}}"
{{- range $k, $v := .Attrs}} {{$k}}="{{$v}}"{{end -}}
>{{.Text}}</{{$tag}}>
{{end}}
1. Save the file
Drop badge.ex into lib/my_app_web/components/.
2. Use it
<.badge>New</.badge>
<.badge variant="destructive">Unstable</.badge>
<.badge as="a" href={~p"/docs"}>Docs</.badge>View source
defmodule ShadcnHtmx.Components.Badge do
@moduledoc """
Badge — shadcn-htmx, htmx v4 + Tailwind v4 for Phoenix.
Mirrors registry/ui/badge.tsx. Non-interactive visual marker; renders
as `<span>` by default. Pass `as="a"` + `href` for a link badge,
`as="button"` for a clickable one.
Accessibility: badge text is the accessible name. Use `aria-label` for
icon-only badges. Status badges that change in place should live inside
an `aria-live` region (see Alert / Toast).
"""
use Phoenix.Component
@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"
@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"
}
attr :variant, :string,
default: "default",
values: ~w(default secondary destructive outline ghost link)
attr :as, :string, default: "span", values: ~w(span a div button)
attr :href, :string, default: nil
attr :class, :string, default: nil
attr :rest, :global
slot :inner_block, required: true
def badge(assigns) do
assigns =
assigns
|> assign(:variant_class, Map.fetch!(@variants, assigns.variant))
|> assign(:base_class, @base)
~H"""
<.dynamic_tag
tag_name={@as}
data-slot="badge"
data-variant={@variant}
href={if @as == "a", do: @href}
class={[@base_class, @variant_class, @class]}
{@rest}
>
{render_slot(@inner_block)}
</.dynamic_tag>
"""
end
end
1. Save the file
Tailwind v4 utilities only; no script.
2. Use it
<span data-slot="badge" data-variant="default"
class="inline-flex w-fit shrink-0 items-center …">
New
</span>View source
<!--
shadcn-htmx — raw HTML badge snippets.
BASE (all variants):
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
-->
<!-- default -->
<span data-slot="badge" data-variant="default"
class="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 bg-primary text-primary-foreground">
New
</span>
<!-- secondary -->
<span data-slot="badge" data-variant="secondary"
class="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 bg-secondary text-secondary-foreground">
Beta
</span>
<!-- destructive -->
<span data-slot="badge" data-variant="destructive"
class="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 bg-destructive text-white focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40">
Unstable
</span>
<!-- outline -->
<span data-slot="badge" data-variant="outline"
class="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 border-border text-foreground">
Draft
</span>
<!-- ghost -->
<span data-slot="badge" data-variant="ghost"
class="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 text-foreground">
Pinned
</span>
<!-- Link variant (as anchor) -->
<a href="/docs/badge" data-slot="badge" data-variant="link"
class="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 text-primary underline-offset-4 hover:underline">
Read docs
</a>
<!-- Icon-only badge — set aria-label so it has an accessible name -->
<span data-slot="badge" data-variant="destructive" aria-label="3 unread notifications"
class="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 bg-destructive text-white">
3
</span>
Examples
Variants — six on-brand colours
Same six variants as Button, mapped to the badge's pill shape.
Variant choice carries meaning: destructive for errors / removal, outline for low-emphasis tags, secondary for neutral counts. Don't over-decorate — a single accent per row reads cleanly, three competes for attention.
<Badge>Default</Badge>
<Badge variant="secondary">Secondary</Badge>
<Badge variant="destructive">Destructive</Badge>
<Badge variant="outline">Outline</Badge>
<Badge variant="ghost">Ghost</Badge>{{ badge("Default") }}
{{ badge("Secondary", variant="secondary") }}
{{ badge("Destructive", variant="destructive") }}
{{ badge("Outline", variant="outline") }}
{{ badge("Ghost", variant="ghost") }}{{template "badge" (dict "Text" "Default")}}
{{template "badge" (dict "Text" "Secondary" "Variant" "secondary")}}
{{template "badge" (dict "Text" "Destructive" "Variant" "destructive")}}
{{template "badge" (dict "Text" "Outline" "Variant" "outline")}}
{{template "badge" (dict "Text" "Ghost" "Variant" "ghost")}}<.badge>Default</.badge>
<.badge variant="secondary">Secondary</.badge>
<.badge variant="destructive">Destructive</.badge>
<.badge variant="outline">Outline</.badge>
<.badge variant="ghost">Ghost</.badge><div class="flex flex-wrap items-center justify-center gap-2">
<span data-slot="badge" data-variant="default" class="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 bg-primary text-primary-foreground [a&]:hover:bg-primary/90">Default</span>
<span data-slot="badge" data-variant="secondary" class="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 bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90">Secondary</span>
<span data-slot="badge" data-variant="destructive" class="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 bg-destructive text-white focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40 [a&]:hover:bg-destructive/90">Destructive</span>
<span data-slot="badge" data-variant="outline" class="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 border-border text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground">Outline</span>
<span data-slot="badge" data-variant="ghost" class="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 [a&]:hover:bg-accent [a&]:hover:text-accent-foreground">Ghost</span>
</div>Further reading
Link badge — anchor with badge styling
Pass href to render as <a> with hover state. Use variant="link" for an underlined text-only style.
When the badge is clickable, render a real link (<a>) so the platform handles cursor, focus ring, middle-click, and assistive-tech link role. The [a&]: Tailwind selector only applies hover styles when the badge is rendered as an anchor — non-link badges stay static.
<Badge as="a" href="/docs/badge">Read the docs</Badge>
<Badge as="a" href="https://htmx.org" variant="link">htmx.org</Badge>{{ badge("Read the docs", tag="a", href="/docs/badge") }}
{{ badge("htmx.org", tag="a", href="https://htmx.org", variant="link") }}{{template "badge" (dict "Text" "Read the docs" "Tag" "a" "Href" "/docs/badge")}}
{{template "badge" (dict "Text" "htmx.org" "Tag" "a" "Href" "https://htmx.org" "Variant" "link")}}<.badge as="a" href={~p"/docs/badge"}>Read the docs</.badge>
<.badge as="a" href="https://htmx.org" variant="link">htmx.org</.badge><div class="flex flex-wrap items-center justify-center gap-2">
<a href="/docs/badge" data-slot="badge" data-variant="default" class="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 bg-primary text-primary-foreground [a&]:hover:bg-primary/90">Read the docs</a>
<a href="https://htmx.org" data-slot="badge" data-variant="link" class="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 text-primary underline-offset-4 [a&]:hover:underline">htmx.org</a>
</div>Further reading
Icon-only — counter with accessible name
Number-only badges (notification counters) need ariaLabel so screen readers announce "3 unread notifications" instead of just "3".
WCAG 1.3.1 (Info and Relationships) says programmatic meaning must match visual meaning. A "3" by itself is ambiguous — pair it with ariaLabel="3 unread notifications" so the count's context comes through. Counters that update on the client (htmx swap, push) should also live in a live-region (see Alert) so changes are announced.
<Badge variant="destructive" ariaLabel="3 unread notifications">3</Badge>
<Badge variant="secondary" ariaLabel="12 items">12</Badge>
<Badge variant="default" ariaLabel="99 plus messages">99+</Badge>{{ badge("3", variant="destructive", aria_label="3 unread notifications") }}
{{ badge("12", variant="secondary", aria_label="12 items") }}
{{ badge("99+", variant="default", aria_label="99 plus messages") }}{{template "badge" (dict "Text" "3" "Variant" "destructive" "AriaLabel" "3 unread notifications")}}
{{template "badge" (dict "Text" "12" "Variant" "secondary" "AriaLabel" "12 items")}}
{{template "badge" (dict "Text" "99+" "Variant" "default" "AriaLabel" "99 plus messages")}}<.badge variant="destructive" aria-label="3 unread notifications">3</.badge>
<.badge variant="secondary" aria-label="12 items">12</.badge>
<.badge variant="default" aria-label="99 plus messages">99+</.badge><div class="flex flex-wrap items-center justify-center gap-3">
<span data-slot="badge" data-variant="destructive" class="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 bg-destructive text-white focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40 [a&]:hover:bg-destructive/90" aria-label="3 unread notifications">3</span>
<span data-slot="badge" data-variant="secondary" class="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 bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90" aria-label="12 items">12</span>
<span data-slot="badge" data-variant="default" class="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 bg-primary text-primary-foreground [a&]:hover:bg-primary/90" aria-label="99 plus messages">99+</span>
</div>Further reading
With icon — SVG + label
An inline SVG plus a short label. The SVG sits inside the badge as a child; the [&>svg]:size-3 utility sizes it automatically.
Decorative SVGs should carry aria-hidden="true" so AT doesn't double-announce. If the icon is the entire badge content (no text), drop aria-hidden and add role="img" + aria-label to the badge itself instead.
<Badge>
<CheckIcon aria-hidden="true" /> Saved
</Badge>
<Badge variant="destructive">
<XCircleIcon aria-hidden="true" /> Failed
</Badge>{{ badge("Saved", variant="default") }} {# wrap an <svg> inside the macro call if your macro accepts a body #}
{{ badge("Failed", variant="destructive") }}{{template "badge" (dict "Text" "Saved")}}
{{template "badge" (dict "Text" "Failed" "Variant" "destructive")}}<.badge>
<.icon name="hero-check" aria-hidden="true" /> Saved
</.badge><div class="flex flex-wrap items-center justify-center gap-2">
<span data-slot="badge" data-variant="default" class="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 bg-primary text-primary-foreground [a&]:hover:bg-primary/90">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<polyline points="20 6 9 17 4 12">
</polyline>
</svg>
Saved
</span>
<span data-slot="badge" data-variant="destructive" class="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 bg-destructive text-white focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40 [a&]:hover:bg-destructive/90">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<circle cx="12" cy="12" r="10">
</circle>
<line x1="15" y1="9" x2="9" y2="15">
</line>
<line x1="9" y1="9" x2="15" y2="15">
</line>
</svg>
Failed
</span>
<span data-slot="badge" data-variant="outline" class="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 border-border text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<circle cx="12" cy="12" r="10">
</circle>
<line x1="12" y1="8" x2="12" y2="12">
</line>
<line x1="12" y1="16" x2="12.01" y2="16">
</line>
</svg>
Pending
</span>
</div>Further reading
API Reference
<Badge>
| Prop | Type | Default | Description |
|---|---|---|---|
target / rel / download / referrerpolicy / hreflang / type / ping | string | — | Anchor attributes forwarded to the underlying <a> when as="a" (or via asChild). Use target="_blank" (with rel="noopener") for new-tab link badges, and download for download badges.MDN<a> |
hx-* | any | — | Any htmx attribute. Forwarded onto the underlying element.htmxAttribute reference |
variant | "default"|"secondary"|"destructive"|"outline"|"ghost"|"link" | "default" | Visual variant. |
as | "span"|"a"|"div"|"button" | "span" | Element to render. Defaults to span (or a if href provided). |
href | string | — | When set, renders as <a> and adds hover state. |
ariaLabel | string | — | Required for icon-only badges (e.g. "3 unread notifications"). |
asChild | boolean | — | Render child instead, merging badge classes onto it. |
class | string | — | Extra Tailwind classes appended to the root element. |