- Accordion
- AppShell
- Autocomplete
- Badge
- Blobatar
- BoringAvatar
- Button
- Calendar
- Checkbox
- DataGrid
- Date Picker
- Date Range Picker
- DateTime
- Dialog
- Drawer
- Dropdown
- Empty
- Error State
- Frame
- Input
- Input Amount
- Input Group
- Input OTP
- Input Password
- Input Phone
- Input Search
- Input Secret
- Loading
- PageContainer
- PageHeader
- Popover
- Select
- StatsCard
- Switch
- Tabs
- TextArea
- Toast
- Tooltip
Playground
"use client";
import { Badge } from "@/components/ui/badge";
export const BadgeDemo = () => (
<Badge variant="default" size="default" radius="default">
{"Ready"}
</Badge>
);Installation
Initialize shadcn in your React project, then install this component from the Vandor UI registry:
$ pnpm dlx shadcn@latest add https://vandor-ui.vercel.app/r/badge.json
Dependencies
Installed automatically by the registry CLI. For manual setup:
- @base-ui/react@^1.8.0
- class-variance-authority@^0.7.1
- cn@^0.4.0
$ pnpm add @base-ui/react@^1.8.0 class-variance-authority@^0.7.1 cn@^0.4.0Usage
import { Badge } from "@/components/ui/badge";
export const Demo = () => <Badge variant="success-light">Ready</Badge>;Use your project's configured UI alias if it differs from @/components/ui. Badge is a presentational label, not a data-fetching component. Applications own the status and its text.
Examples
Variants
Use solid styles for emphasis, light styles for quiet status indicators, and outline styles for metadata. Color never replaces a meaningful label.
import { Badge } from "@/components/ui/badge";
export const BadgeVariants = () => (
<div className="flex flex-wrap items-center justify-center gap-2">
<Badge>Default</Badge>
<Badge variant="secondary">Secondary</Badge>
<Badge variant="outline">Outline</Badge>
<Badge variant="success">Success</Badge>
<Badge variant="info">Info</Badge>
<Badge variant="warning">Warning</Badge>
<Badge variant="destructive">Error</Badge>
<Badge variant="success-light">Success light</Badge>
<Badge variant="info-light">Info light</Badge>
<Badge variant="warning-light">Warning light</Badge>
<Badge variant="destructive-light">Error light</Badge>
<Badge variant="success-outline">Success outline</Badge>
<Badge variant="info-outline">Info outline</Badge>
<Badge variant="warning-outline">Warning outline</Badge>
<Badge variant="destructive-outline">Error outline</Badge>
</div>
);The primary family uses Vandor's primary tokens. focus, focus-light, and focus-outline are ReUI-compatible aliases for the primary family, not keyboard-focus states. invert uses foreground/background tokens instead of an additional color scheme.
Sizes and Pill Radius
The five sizes are xs, sm, default, lg, and xl. Prefer default or xl for status text. The smaller sizes are intended for dense metadata. radius="default" uses the theme's rounded-md; radius="full" makes a pill.
import { Badge } from "@/components/ui/badge";
export const BadgeSizes = () => (
<div className="flex flex-wrap items-center justify-center gap-2">
{(["xs", "sm", "default", "lg", "xl"] as const).map((size) => (
<Badge key={size} variant="secondary" size={size}>
{size}
</Badge>
))}
<Badge variant="outline" radius="full">
Pill
</Badge>
</div>
);Icons, Spinner, and Links
Compose icons, dots, or a spinner as children. These are not Badge props. Icons inherit the current color, and SVG sizing is handled by Badge unless the icon supplies an explicit size-* class.
import { ArrowUpRight, Check, LoaderCircle } from "lucide-react";
import { Badge } from "@/components/ui/badge";
export const BadgeComposition = () => (
<div className="flex flex-wrap items-center justify-center gap-2">
<Badge variant="success-light">
<Check aria-hidden="true" />
Verified
</Badge>
<Badge variant="info-light" role="status" aria-live="polite">
<LoaderCircle aria-hidden="true" className="motion-safe:animate-spin" />
Refreshing
</Badge>
<Badge variant="outline" render={<a href="#behavior--accessibility" aria-label="Accessibility">Accessibility
<ArrowUpRight aria-hidden="true" /></a>} />
</div>
);Use Base UI's render to replace the span with a link. Badge has no asChild, disabled, or isLoading prop. Give interactive elements their native semantics; use Button for actions rather than putting click behavior on a span. Badge merges public refs and handlers with the replacement element.
Theme and Consumer Setup
Requires React 19, Tailwind CSS 4, and the standard shadcn theme tokens. CLI installation includes Base UI 1.8+, class-variance-authority, cn, and semantic CSS variables for both light and dark themes. It installs a single Badge source file with its MIT notice, not another component catalog.
Additional tokens are --success, --info, and --warning, each with -foreground for solid badges and -text for light/outline badges. --destructive-foreground and --destructive-text complete the destructive family. Tailwind's @theme inline mappings are included by the registry's cssVars.theme. The distinction avoids using white text on a pale status background. Review existing custom token values when installing into an already themed project.
If copying the Source manually, also copy the cssVars.theme, cssVars.light, and cssVars.dark entries from the Badge registry artifact into your Tailwind theme, :root, and .dark respectively. Copying the TSX alone does not supply the semantic colors. Preserve its license comment and install all listed dependencies. The icon/spinner examples additionally require lucide-react; Badge itself does not.
Storybook Stories (Optional)
Install Badge first, then install the stories separately:
pnpm dlx shadcn@latest add https://vandor-ui.vercel.app/r/badge-stories.jsonThe stories-only item does not reinstall Badge, modify .storybook/, or install/upgrade Storybook packages. Use @storybook/react matching your existing Storybook setup (verified against 10.6.1), plus lucide-react for the examples. Include **/*.stories.tsx in story discovery and import your Tailwind/theme CSS in preview setup. Light/dark decorators belong to the consumer. Stories include variants, sizes, pill, dot, icon, spinner, link, and long labels; no addons are required by the story source.
Behavior & Accessibility
- Badge renders a
spanby default and is not focusable. It does not announce every static label automatically. - For changing async status, the application can supply
role="status"andaria-live="polite". Reserve a consistent toolbar slot when replacing status text to avoid layout jumps. - Use “Live” only when your application actually updates data automatically. Successful fetches alone do not establish a live connection.
- Hide decorative icons/dots with
aria-hidden="true". Keep the status readable as text. motion-safe:animate-spinin the spinner example stops animation when reduced motion is preferred.- Links retain native keyboard navigation and a visible focus ring.
variant="outline"alone does not make a Badge a link. - Labels stay on one line. For constrained layouts, keep labels short or provide a scrollable parent. Badge does not silently truncate meaningful status text.
Props
| Prop | Type / Description | Default |
|---|---|---|
| children | ReactNodeBadge content. Keep visible text alongside status icons or dots. | Not set |
| className | stringClasses merged with variant, size and radius defaults. | Not set |
| radius | "default" | "full"Token-based rounded-md or a fully rounded pill. | "default" |
| ref | Ref<HTMLSpanElement>React 19 ref to the rendered element, composed with a render element's ref. | Not set |
| render | ReactElement | ComponentRenderFnBase UI element or render function replacing the span. Preserves the replacement element's attributes, handlers and ref. No asChild prop. | Not set |
| size | "xs" | "sm" | "default" | "lg" | "xl"Compact label dimensions. Prefer default or xl for status text; xs/sm are dense metadata sizes. | "default" |
| variant | "default" | "secondary" | "outline" | "success" | "info" | "warning" | "destructive" | "focus" | "invert" | "primary-light" | "success-light" | "info-light" | "warning-light" | "destructive-light" | "focus-light" | "invert-light" | "primary-outline" | "success-outline" | "info-outline" | "warning-outline" | "destructive-outline" | "focus-outline" | "invert-outline"Semantic solid, light or outline appearance. Focus uses Vandor's primary tokens; invert uses foreground/background. Appearance does not imply live data or interactive behavior. | "default" |
Badge forwards native span attributes, handlers and React 19 refs through Base UI useRender. Use render for links or other elements; Badge does not add button semantics, disabled behavior, or an automatic live region.
Source
"use client";
/**
* Adapted from ReUI Badge (https://github.com/keenthemes/reui).
* MIT License
* Copyright (c) 2025 Keenthemes Inc
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import { mergeProps } from "@base-ui/react/merge-props";
import { useRender } from "@base-ui/react/use-render";
import { cva } from "class-variance-authority";
import type { VariantProps } from "class-variance-authority";
import { cn } from "cn";
const badgeVariants = cva(
"relative inline-flex w-fit shrink-0 items-center justify-center gap-1 border border-transparent font-medium whitespace-nowrap outline-none transition-[color,box-shadow] focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=size-])]:size-3",
{
defaultVariants: { radius: "default", size: "default", variant: "default" },
variants: {
radius: { default: "rounded-md", full: "rounded-full" },
size: {
default: "h-5 min-w-5 px-1.5 py-0.5 text-xs",
lg: "h-5.5 min-w-5.5 px-1.5 py-0.5 text-xs",
sm: "h-4.5 min-w-4.5 px-1 py-0.25 text-[0.625rem] leading-none",
xl: "h-6 min-w-6 gap-1.5 px-2 py-0.75 text-sm",
xs: "h-4 min-w-4 px-1 py-0.25 text-[0.6rem] leading-none",
},
variant: {
default: "bg-primary text-primary-foreground",
destructive: "bg-destructive text-destructive-foreground",
"destructive-light":
"border-destructive/20 bg-destructive/10 text-destructive-text",
"destructive-outline":
"border-border bg-background text-destructive-text",
focus: "bg-primary text-primary-foreground",
"focus-light": "border-primary/20 bg-primary/10 text-primary",
"focus-outline": "border-border bg-background text-primary",
info: "bg-info text-info-foreground",
"info-light": "border-info/20 bg-info/10 text-info-text",
"info-outline": "border-border bg-background text-info-text",
invert: "bg-foreground text-background",
"invert-light": "border-foreground/20 bg-foreground/10 text-foreground",
"invert-outline": "border-border bg-background text-foreground",
outline: "border-border bg-transparent text-foreground",
"primary-light": "border-primary/20 bg-primary/10 text-primary",
"primary-outline": "border-border bg-background text-primary",
secondary: "bg-secondary text-secondary-foreground",
success: "bg-success text-success-foreground",
"success-light": "border-success/20 bg-success/10 text-success-text",
"success-outline": "border-border bg-background text-success-text",
warning: "bg-warning text-warning-foreground",
"warning-light": "border-warning/20 bg-warning/10 text-warning-text",
"warning-outline": "border-border bg-background text-warning-text",
},
},
}
);
type BadgeProps = useRender.ComponentProps<"span"> &
VariantProps<typeof badgeVariants>;
const Badge = ({
className,
radius = "default",
ref,
render,
size = "default",
variant = "default",
...props
}: BadgeProps) => {
const defaultProps = {
className: cn(badgeVariants({ radius, size, variant }), className),
"data-radius": radius,
"data-size": size,
"data-slot": "badge",
"data-variant": variant,
};
return useRender({
defaultTagName: "span",
props: mergeProps<"span">(defaultProps, props),
ref,
render,
});
};
export { Badge, badgeVariants };
export type { BadgeProps };
Credits
- ReUI: Badge implementation adapted under the MIT license. Vandor supplies token-based radius, semantic foreground/text colors, public ref handling, and portable registry packaging. The full license notice is retained in the distributed source.