Vela UI Docsvenizia.ai

Badge

Displays a badge or a component that looks like a badge.

npx shadcn@latest add @velaui/badge
npx shadcn@latest add @velaui/badge

Examples

  • Default
  • Count
  • Verified
  • Expiring
  • In progress
  • Draft
  • Blocked
  • Changelog
  • Failed

View code

Badge · Default

Installation

pnpm dlx velaui add @velaui/badge
pnpm dlx velaui add @velaui/badge

Code

badge-default.tsx
import { Badge } from "@/components/ui/badge";

export default function Example() {
  return <Badge>Badge</Badge>;
}

View code

Badge · Count

Installation

pnpm dlx velaui add @velaui/badge
pnpm dlx velaui add @velaui/badge

Code

badge-count.tsx
import { Badge } from "@/components/ui/badge";

export default function Example() {
  return (
    <Badge variant="solid" color="destructive">
      12
    </Badge>
  );
}

View code

Badge · Verified

Installation

pnpm dlx velaui add @velaui/badge
pnpm dlx velaui add @velaui/badge

Code

badge-verified.tsx
import { Check01 } from "@/components/icons/check-01";

import { Badge } from "@/components/ui/badge";

export default function Example() {
  return (
    <Badge variant="subtle" color="success" size="lg">
      <Check01 data-icon="inline-start" />
      Verified
    </Badge>
  );
}

View code

Badge · Expiring

Installation

pnpm dlx velaui add @velaui/badge
pnpm dlx velaui add @velaui/badge

Code

badge-expiring.tsx
import { Badge } from "@/components/ui/badge";

export default function Example() {
  return (
    <Badge variant="outline" color="warning">
      Expiring soon
    </Badge>
  );
}

View code

Badge · In progress

Installation

pnpm dlx velaui add @velaui/badge
pnpm dlx velaui add @velaui/badge

Code

badge-in-progress.tsx
import { Badge } from "@/components/ui/badge";

export default function Example() {
  return (
    <Badge variant="ghost" color="info" dot>
      In Progress
    </Badge>
  );
}

View code

Badge · Draft

Installation

pnpm dlx velaui add @velaui/badge
pnpm dlx velaui add @velaui/badge

Code

badge-draft.tsx
import { Badge } from "@/components/ui/badge";

export default function Example() {
  return (
    <Badge variant="subtle" color="neutral" size="sm">
      Draft
    </Badge>
  );
}

View code

Installation

Code

badge-docs-link.tsx

View code

Badge · Blocked

Installation

pnpm dlx velaui add @velaui/badge
pnpm dlx velaui add @velaui/badge

Code

badge-blocked.tsx
import { Badge } from "@/components/ui/badge";

export default function Example() {
  return (
    <Badge variant="ghost" color="destructive" size="lg" dot>
      Blocked
    </Badge>
  );
}

View code

Badge · Changelog

Installation

pnpm dlx velaui add @velaui/badge
pnpm dlx velaui add @velaui/badge

Code

badge-changelog.tsx
import { ArrowUpRight } from "@/components/icons/arrow-up-right";

import { Badge } from "@/components/ui/badge";

export default function Example() {
  return (
    <Badge asChild variant="solid" color="info">
      <a href="#">
        Read changelog
        <ArrowUpRight data-icon="inline-end" />
      </a>
    </Badge>
  );
}
import { ArrowUpRight } from "@/components/icons/arrow-up-right";

import { Badge } from "@/components/ui/badge";

export default function Example() {
  return (
    <Badge render={<a href="#" />} variant="solid" color="info">
      Read changelog
      <ArrowUpRight data-icon="inline-end" />
    </Badge>
  );
}

View code

Badge · Failed

Installation

pnpm dlx velaui add @velaui/badge
pnpm dlx velaui add @velaui/badge

Code

badge-failed.tsx
import { Badge } from "@/components/ui/badge";

export default function Example() {
  return (
    <Badge variant="subtle" color="destructive" size="sm">
      Failed
    </Badge>
  );
}

Usage

import { Badge } from "@/components/ui/badge";

export default function Example() {
  return <Badge>Badge</Badge>;
}

Props

PropTypeDefault
asChildbooleanfalse
color"info" | "success" | "warning" | "destructive" | "neutral" | "inverse" | "accent""neutral"
dotbooleanfalse
size"sm" | "md" | "lg""md"
variant"outline" | "solid" | "subtle" | "ghost""solid"

Takes these and every prop of BadgeProps.

PropTypeDefault
color"info" | "success" | "warning" | "destructive" | "neutral" | "inverse" | "accent""neutral"
dotbooleanfalse
size"sm" | "md" | "lg""md"
variant"outline" | "solid" | "subtle" | "ghost""solid"

Takes these and every prop of BadgeProps.

Dependencies

npm
class-variance-authority@0.7.1radix-ui@1.6.0
Installs with it
nothing else
npm
@base-ui/react@1.6.0class-variance-authority@0.7.1
Installs with it
nothing else

Source

components/ui/badge.tsx
/* eslint-disable react-refresh/only-export-components */
import * as React from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { Slot } from "radix-ui";

import { cn } from "@/lib/utils";

const badgeVariants = cva(
  "group/badge inline-flex w-fit shrink-0 items-center justify-center rounded-full border border-transparent font-medium whitespace-nowrap transition-all focus-visible:border-ring-neutral-outline focus-visible:ring-3 focus-visible:ring-ring-neutral-shadow [&>svg]:pointer-events-none [&>svg]:shrink-0",
  {
    variants: {
      variant: {
        solid: "",
        subtle: "",
        outline: "",
        ghost: "",
      },
      color: {
        neutral: "",
        inverse: "",
        info: "",
        success: "",
        warning: "",
        destructive: "",
        accent: "",
      },
      size: {
        sm: "h-4 gap-0.5 px-1.5 py-0.5 text-[10px] leading-[1.3] has-data-[icon=inline-end]:pr-1 has-data-[icon=inline-start]:pl-1 [&>svg]:size-3",
        md: "h-5 gap-1 px-2 py-1 text-xs leading-[1.3] has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&>svg]:size-3.5",
        lg: "h-6 gap-1 px-2 py-1 text-sm leading-[1.4] has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&>svg]:size-4",
      },
    },
    compoundVariants: [
      // Solid — filled surface in the colour, label inverted against it
      {
        variant: "solid",
        color: "neutral",
        className: "bg-bg-foreground text-text-inverse",
      },
      {
        variant: "solid",
        color: "inverse",
        className: "bg-bg-main text-text-strong",
      },
      {
        variant: "solid",
        color: "info",
        className: "bg-info-middle text-base-white",
      },
      {
        variant: "solid",
        color: "success",
        className: "bg-success-middle text-base-white",
      },
      {
        variant: "solid",
        color: "warning",
        className: "bg-warning-middle text-base-white",
      },
      {
        variant: "solid",
        color: "destructive",
        className: "bg-negative-middle text-base-white",
      },
      {
        variant: "solid",
        color: "accent",
        className: "bg-accent-default text-base-white",
      },

      // Subtle — tinted surface, label in the colour itself
      {
        variant: "subtle",
        color: "neutral",
        className: "bg-bg-surface-1 text-text-strong",
      },
      {
        variant: "subtle",
        color: "inverse",
        className: "bg-bg-surface-inverse text-text-inverse",
      },
      {
        variant: "subtle",
        color: "info",
        className: "bg-info text-info-strong",
      },
      {
        variant: "subtle",
        color: "success",
        className: "bg-success text-success-middle",
      },
      {
        variant: "subtle",
        color: "warning",
        className: "bg-warning text-warning-middle",
      },
      {
        variant: "subtle",
        color: "destructive",
        className: "bg-negative text-negative-middle",
      },
      {
        variant: "subtle",
        color: "accent",
        className: "bg-teal-50 text-accent-default",
      },

      // Outline — no surface, 1px border and label in the colour
      {
        variant: "outline",
        color: "neutral",
        className: "border-border-default text-text-strong",
      },
      {
        variant: "outline",
        color: "inverse",
        className: "border-border-inverse text-text-inverse",
      },
      {
        variant: "outline",
        color: "info",
        className: "border-info-middle text-info-middle",
      },
      {
        variant: "outline",
        color: "success",
        className: "border-success-middle text-success-middle",
      },
      {
        variant: "outline",
        color: "warning",
        className: "border-warning-middle text-warning-middle",
      },
      {
        variant: "outline",
        color: "destructive",
        className: "border-negative-middle text-negative-middle",
      },
      {
        variant: "outline",
        color: "accent",
        className: "border-accent-default text-accent-default",
      },

      // Ghost — transparent surface with the neutral label colour; hover fills
      // in with the matching subtle surface.
      {
        variant: "ghost",
        color: "neutral",
        className: "text-text-strong hover:bg-bg-surface-1",
      },
      {
        variant: "ghost",
        color: "inverse",
        className: "text-text-inverse hover:bg-bg-surface-inverse",
      },
      {
        variant: "ghost",
        color: "info",
        className: "text-text-strong hover:bg-info",
      },
      {
        variant: "ghost",
        color: "success",
        className: "text-text-strong hover:bg-success",
      },
      {
        variant: "ghost",
        color: "warning",
        className: "text-text-strong hover:bg-warning",
      },
      {
        variant: "ghost",
        color: "destructive",
        className: "text-text-strong hover:bg-negative",
      },
      {
        variant: "ghost",
        color: "accent",
        className: "text-accent-default hover:bg-teal-50",
      },
      { variant: "ghost", className: "bg-transparent" },
    ],
    defaultVariants: {
      variant: "solid",
      color: "neutral",
      size: "md",
    },
  },
);

// The dot sits in a slightly larger box so it keeps a small optical gap from
// its neighbours. It takes the badge's own label colour.
const badgeDotVariants = cva(
  "flex shrink-0 items-center justify-center *:rounded-full *:bg-current",
  {
    variants: {
      size: {
        sm: "size-[9px] *:size-1.5",
        md: "size-[11px] *:size-[7px]",
        lg: "size-[14px] *:size-[9px]",
      },
    },
    defaultVariants: {
      size: "md",
    },
  },
);

export type BadgeProps = React.ComponentProps<"span"> &
  VariantProps<typeof badgeVariants> & { dot?: boolean; asChild?: boolean };

function Badge({
  className,
  variant = "solid",
  color = "neutral",
  size = "md",
  dot = false,
  asChild = false,
  children,
  ...props
}: BadgeProps) {
  const Comp = asChild ? Slot.Root : "span";

  return (
    <Comp
      data-slot="badge"
      data-variant={variant}
      data-color={color}
      data-size={size}
      className={cn(badgeVariants({ variant, color, size }), className)}
      {...props}
    >
      {dot && !asChild ? (
        <>
          <span
            data-slot="badge-dot"
            className={cn(badgeDotVariants({ size }))}
          >
            <span />
          </span>
          {children}
        </>
      ) : (
        children
      )}
    </Comp>
  );
}

export { Badge, badgeVariants, badgeDotVariants };
components/ui/badge.tsx
/* eslint-disable react-refresh/only-export-components */
import { mergeProps } from "@base-ui/react/merge-props";
import { useRender } from "@base-ui/react/use-render";
import { cva, type VariantProps } from "class-variance-authority";

import { cn } from "@/lib/utils";

const badgeVariants = cva(
  "group/badge inline-flex w-fit shrink-0 items-center justify-center rounded-full border border-transparent font-medium whitespace-nowrap transition-all focus-visible:border-ring-neutral-outline focus-visible:ring-3 focus-visible:ring-ring-neutral-shadow [&>svg]:pointer-events-none [&>svg]:shrink-0",
  {
    variants: {
      variant: {
        solid: "",
        subtle: "",
        outline: "",
        ghost: "",
      },
      color: {
        neutral: "",
        inverse: "",
        info: "",
        success: "",
        warning: "",
        destructive: "",
        accent: "",
      },
      size: {
        sm: "h-4 gap-0.5 px-1.5 py-0.5 text-[10px] leading-[1.3] has-data-[icon=inline-end]:pr-1 has-data-[icon=inline-start]:pl-1 [&>svg]:size-3",
        md: "h-5 gap-1 px-2 py-1 text-xs leading-[1.3] has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&>svg]:size-3.5",
        lg: "h-6 gap-1 px-2 py-1 text-sm leading-[1.4] has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&>svg]:size-4",
      },
    },
    compoundVariants: [
      // Solid — filled surface in the colour, label inverted against it
      {
        variant: "solid",
        color: "neutral",
        className: "bg-bg-foreground text-text-inverse",
      },
      {
        variant: "solid",
        color: "inverse",
        className: "bg-bg-main text-text-strong",
      },
      {
        variant: "solid",
        color: "info",
        className: "bg-info-middle text-base-white",
      },
      {
        variant: "solid",
        color: "success",
        className: "bg-success-middle text-base-white",
      },
      {
        variant: "solid",
        color: "warning",
        className: "bg-warning-middle text-base-white",
      },
      {
        variant: "solid",
        color: "destructive",
        className: "bg-negative-middle text-base-white",
      },
      {
        variant: "solid",
        color: "accent",
        className: "bg-accent-default text-base-white",
      },

      // Subtle — tinted surface, label in the colour itself
      {
        variant: "subtle",
        color: "neutral",
        className: "bg-bg-surface-1 text-text-strong",
      },
      {
        variant: "subtle",
        color: "inverse",
        className: "bg-bg-surface-inverse text-text-inverse",
      },
      {
        variant: "subtle",
        color: "info",
        className: "bg-info text-info-strong",
      },
      {
        variant: "subtle",
        color: "success",
        className: "bg-success text-success-middle",
      },
      {
        variant: "subtle",
        color: "warning",
        className: "bg-warning text-warning-middle",
      },
      {
        variant: "subtle",
        color: "destructive",
        className: "bg-negative text-negative-middle",
      },
      {
        variant: "subtle",
        color: "accent",
        className: "bg-teal-50 text-accent-default",
      },

      // Outline — no surface, 1px border and label in the colour
      {
        variant: "outline",
        color: "neutral",
        className: "border-border-default text-text-strong",
      },
      {
        variant: "outline",
        color: "inverse",
        className: "border-border-inverse text-text-inverse",
      },
      {
        variant: "outline",
        color: "info",
        className: "border-info-middle text-info-middle",
      },
      {
        variant: "outline",
        color: "success",
        className: "border-success-middle text-success-middle",
      },
      {
        variant: "outline",
        color: "warning",
        className: "border-warning-middle text-warning-middle",
      },
      {
        variant: "outline",
        color: "destructive",
        className: "border-negative-middle text-negative-middle",
      },
      {
        variant: "outline",
        color: "accent",
        className: "border-accent-default text-accent-default",
      },

      // Ghost — transparent surface with the neutral label colour; hover fills
      // in with the matching subtle surface.
      {
        variant: "ghost",
        color: "neutral",
        className: "text-text-strong hover:bg-bg-surface-1",
      },
      {
        variant: "ghost",
        color: "inverse",
        className: "text-text-inverse hover:bg-bg-surface-inverse",
      },
      {
        variant: "ghost",
        color: "info",
        className: "text-text-strong hover:bg-info",
      },
      {
        variant: "ghost",
        color: "success",
        className: "text-text-strong hover:bg-success",
      },
      {
        variant: "ghost",
        color: "warning",
        className: "text-text-strong hover:bg-warning",
      },
      {
        variant: "ghost",
        color: "destructive",
        className: "text-text-strong hover:bg-negative",
      },
      {
        variant: "ghost",
        color: "accent",
        className: "text-accent-default hover:bg-teal-50",
      },
      { variant: "ghost", className: "bg-transparent" },
    ],
    defaultVariants: {
      variant: "solid",
      color: "neutral",
      size: "md",
    },
  },
);

// The dot sits in a slightly larger box so it keeps a small optical gap from
// its neighbours. It takes the badge's own label colour.
const badgeDotVariants = cva(
  "flex shrink-0 items-center justify-center *:rounded-full *:bg-current",
  {
    variants: {
      size: {
        sm: "size-[9px] *:size-1.5",
        md: "size-[11px] *:size-[7px]",
        lg: "size-[14px] *:size-[9px]",
      },
    },
    defaultVariants: {
      size: "md",
    },
  },
);

export type BadgeProps = useRender.ComponentProps<"span"> &
  VariantProps<typeof badgeVariants> & { dot?: boolean };

function Badge({
  className,
  variant = "solid",
  color = "neutral",
  size = "md",
  dot = false,
  render,
  children,
  ...props
}: BadgeProps) {
  return useRender({
    defaultTagName: "span",
    props: mergeProps<"span">(
      {
        className: cn(badgeVariants({ variant, color, size }), className),
        children:
          dot && !render ? (
            <>
              <span
                data-slot="badge-dot"
                className={cn(badgeDotVariants({ size }))}
              >
                <span />
              </span>
              {children}
            </>
          ) : (
            children
          ),
      },
      props,
    ),
    render,
    state: {
      slot: "badge",
      variant,
      color,
      size,
    },
  });
}

export { Badge, badgeVariants, badgeDotVariants };