Badges

Compact label components for displaying short pieces of information. Supports various styles, colors, and customization options. previews

Default Badge

A basic badge component for displaying short information.

 
<span
  class="inline-flex items-center font-medium bg-blue-500 dark:bg-blue-600 text-white dark:text-white border-blue-600 dark:border-blue-500 rounded px-2 py-1 text-sm">
  <p>Default</p>
</span>
---
import { Badge } from '@/components/ui/badge';

---

<Badge>
Default
</Badge>
---
// Imports
import { type HTMLAttributes } from "astro/types";
import { tv, type VariantProps } from "@utils/custom-tv";
import Icon from "./Icon.astro";
import {
  getOutlinedClasses,
  type ColorName,
  getDefaultClasses,
  getSoftClasses,
  colorPalette,
} from "@utils/colorUtils";
import { twMerge } from "tailwind-merge";

// PropTypes for documentation
export const propTypes = {
  content: {
    type: "string | JSX.Element",
    description: "The content of the badge",
  },
  icon: {
    type: "string | { name: string; class?: string }",
    description: "The icon to display in the badge",
  },
  iconPosition: {
    type: ["left", "right"],
    description: "The position of the icon",
    default: "left",
  },
  class: {
    type: "string",
    description: "Additional CSS classes to apply to the badge",
  },
  removable: {
    type: "boolean",
    description: "Whether the badge is removable",
    default: false,
  },
  variant: {
    type: ["solid", "soft", "outline"],
    description: "The variant of the badge",
    default: "solid",
  },
  color: {
    type: Object.keys(colorPalette),
    description: "The color scheme for the badge",
    default: "blue",
  },
  shape: {
    type: ["rounded", "square", "pill"],
    description: "The shape of the badge",
    default: "rounded",
  },
  size: {
    type: ["sm", "md", "lg"],
    description: "The size of the badge",
    default: "md",
  },
  maxWidth: {
    type: "string",
    description: "The maximum width of the badge",
  },
  animated: {
    type: "boolean",
    description: "Whether the badge should have animations",
    default: false,
  },
};

// Types and Interfaces
type BadgeVariants = VariantProps<typeof badgeStyles>;

interface Props extends HTMLAttributes<"span">, BadgeVariants {
  content?: string | JSX.Element;
  icon?: string | { name: string; class?: string };
  iconPosition?: "left" | "right";
  class?: string;
  removable?: boolean;
  variant?: "solid" | "soft" | "outline";
  color?: ColorName;
  shape?: "rounded" | "square" | "pill";
  size?: "sm" | "md" | "lg";
  maxWidth?: string;
  animated?: boolean;
}

// Component Logic
const {
  content,
  icon,
  iconPosition = "left",
  variant = "solid",
  color = "blue",
  removable = false,
  shape = "rounded",
  class: className = "",
  size = "md",
  maxWidth,
  animated = false,
} = Astro.props as Props;

// Styles
const badgeStyles = tv({
  base: "inline-flex items-center font-medium",
  variants: {
    variant: {
      solid: getDefaultClasses(color, { includeHover: false }),
      soft: getSoftClasses(color, { includeHover: false }),
      outline: `${getOutlinedClasses(color, { includeHover: false })} border`,
    },
    shape: {
      rounded: "rounded",
      square: "",
      pill: "rounded-full",
    },
    size: {
      sm: "px-2 py-1 text-xs",
      md: "px-2 py-1 text-sm",
      lg: "px-3 py-1 text-base",
    },
    animated: {
      true: "transition-all duration-300 ease-in-out hover:scale-105",
      false: "",
    },
  },
  defaultVariants: {
    variant: "solid",
    shape: "rounded",
    size: "md",
    animated: false,
  },
});

const finalIcon = typeof icon === "string" ? { name: icon, class: "" } : icon;
---

<span
  class={twMerge(
    badgeStyles({ variant, shape, size, animated }),
    maxWidth && `max-w-[${maxWidth}] truncate`,
    className,
  )}
>
  {
    finalIcon && iconPosition === "left" && (
      <Icon
        name={finalIcon.name}
        class={twMerge(
          `mr-1 ${size === "sm" ? "w-3 h-3" : size === "lg" ? "w-5 h-5" : "w-4 h-4"}`,
          finalIcon.class,
        )}
        aria-hidden="true"
      />
    )
  }
  {content && <Fragment set:html={content} />}
  <slot />
  {
    finalIcon && iconPosition === "right" && (
      <Icon
        name={finalIcon.name}
        class={twMerge(
          `ml-1 ${size === "sm" ? "w-3 h-3" : size === "lg" ? "w-5 h-5" : "w-4 h-4"}`,
          finalIcon.class,
        )}
        aria-hidden="true"
      />
    )
  }
  {
    removable && (
      <button
        type="button"
        class={twMerge(
          "ml-1 -mr-1 hover:bg-opacity-10 hover:bg-black focus:outline-none focus:ring-2 focus:ring-offset-2",
          `focus:ring-${color}-500 rounded-${shape === "pill" ? "full" : "sm"}`,
        )}
        aria-label="Remove"
      >
        <Icon
          name="XMarkIcon"
          class={twMerge(
            size === "sm" ? "w-3 h-3" : size === "lg" ? "w-5 h-5" : "w-4 h-4",
          )}
          aria-hidden="true"
        />
      </button>
    )
  }
</span>

Badge Variants

Badges with different variants: solid, soft, and outline.

 
<span
  class="inline-flex items-center font-medium bg-blue-500 dark:bg-blue-600 text-white dark:text-white border-blue-600 dark:border-blue-500 rounded px-2 py-1 text-sm">
  Solid
</span>
<span
  class="inline-flex items-center font-medium bg-blue-100 dark:bg-blue-800 text-blue-700 dark:text-blue-100 border-blue-200 dark:border-blue-600 rounded px-2 py-1 text-sm ml-2">
  Soft
</span>
<span
  class="inline-flex items-center font-medium bg-transparent text-blue-500 dark:text-blue-400 border-blue-500 dark:border-blue-400 border rounded px-2 py-1 text-sm ml-2">
  Outline
</span>
---
import { Badge } from '@/components/ui/badge';

---

<Badge variant="solid">Solid</Badge>
<Badge variant="soft">Soft</Badge>
<Badge variant="outline">Outline</Badge>
---
// Imports
import { type HTMLAttributes } from "astro/types";
import { tv, type VariantProps } from "@utils/custom-tv";
import Icon from "./Icon.astro";
import {
  getOutlinedClasses,
  type ColorName,
  getDefaultClasses,
  getSoftClasses,
  colorPalette,
} from "@utils/colorUtils";
import { twMerge } from "tailwind-merge";

// PropTypes for documentation
export const propTypes = {
  content: {
    type: "string | JSX.Element",
    description: "The content of the badge",
  },
  icon: {
    type: "string | { name: string; class?: string }",
    description: "The icon to display in the badge",
  },
  iconPosition: {
    type: ["left", "right"],
    description: "The position of the icon",
    default: "left",
  },
  class: {
    type: "string",
    description: "Additional CSS classes to apply to the badge",
  },
  removable: {
    type: "boolean",
    description: "Whether the badge is removable",
    default: false,
  },
  variant: {
    type: ["solid", "soft", "outline"],
    description: "The variant of the badge",
    default: "solid",
  },
  color: {
    type: Object.keys(colorPalette),
    description: "The color scheme for the badge",
    default: "blue",
  },
  shape: {
    type: ["rounded", "square", "pill"],
    description: "The shape of the badge",
    default: "rounded",
  },
  size: {
    type: ["sm", "md", "lg"],
    description: "The size of the badge",
    default: "md",
  },
  maxWidth: {
    type: "string",
    description: "The maximum width of the badge",
  },
  animated: {
    type: "boolean",
    description: "Whether the badge should have animations",
    default: false,
  },
};

// Types and Interfaces
type BadgeVariants = VariantProps<typeof badgeStyles>;

interface Props extends HTMLAttributes<"span">, BadgeVariants {
  content?: string | JSX.Element;
  icon?: string | { name: string; class?: string };
  iconPosition?: "left" | "right";
  class?: string;
  removable?: boolean;
  variant?: "solid" | "soft" | "outline";
  color?: ColorName;
  shape?: "rounded" | "square" | "pill";
  size?: "sm" | "md" | "lg";
  maxWidth?: string;
  animated?: boolean;
}

// Component Logic
const {
  content,
  icon,
  iconPosition = "left",
  variant = "solid",
  color = "blue",
  removable = false,
  shape = "rounded",
  class: className = "",
  size = "md",
  maxWidth,
  animated = false,
} = Astro.props as Props;

// Styles
const badgeStyles = tv({
  base: "inline-flex items-center font-medium",
  variants: {
    variant: {
      solid: getDefaultClasses(color, { includeHover: false }),
      soft: getSoftClasses(color, { includeHover: false }),
      outline: `${getOutlinedClasses(color, { includeHover: false })} border`,
    },
    shape: {
      rounded: "rounded",
      square: "",
      pill: "rounded-full",
    },
    size: {
      sm: "px-2 py-1 text-xs",
      md: "px-2 py-1 text-sm",
      lg: "px-3 py-1 text-base",
    },
    animated: {
      true: "transition-all duration-300 ease-in-out hover:scale-105",
      false: "",
    },
  },
  defaultVariants: {
    variant: "solid",
    shape: "rounded",
    size: "md",
    animated: false,
  },
});

const finalIcon = typeof icon === "string" ? { name: icon, class: "" } : icon;
---

<span
  class={twMerge(
    badgeStyles({ variant, shape, size, animated }),
    maxWidth && `max-w-[${maxWidth}] truncate`,
    className,
  )}
>
  {
    finalIcon && iconPosition === "left" && (
      <Icon
        name={finalIcon.name}
        class={twMerge(
          `mr-1 ${size === "sm" ? "w-3 h-3" : size === "lg" ? "w-5 h-5" : "w-4 h-4"}`,
          finalIcon.class,
        )}
        aria-hidden="true"
      />
    )
  }
  {content && <Fragment set:html={content} />}
  <slot />
  {
    finalIcon && iconPosition === "right" && (
      <Icon
        name={finalIcon.name}
        class={twMerge(
          `ml-1 ${size === "sm" ? "w-3 h-3" : size === "lg" ? "w-5 h-5" : "w-4 h-4"}`,
          finalIcon.class,
        )}
        aria-hidden="true"
      />
    )
  }
  {
    removable && (
      <button
        type="button"
        class={twMerge(
          "ml-1 -mr-1 hover:bg-opacity-10 hover:bg-black focus:outline-none focus:ring-2 focus:ring-offset-2",
          `focus:ring-${color}-500 rounded-${shape === "pill" ? "full" : "sm"}`,
        )}
        aria-label="Remove"
      >
        <Icon
          name="XMarkIcon"
          class={twMerge(
            size === "sm" ? "w-3 h-3" : size === "lg" ? "w-5 h-5" : "w-4 h-4",
          )}
          aria-hidden="true"
        />
      </button>
    )
  }
</span>

Badge Shapes

Badges with different shapes: rounded, square, and pill.

 
<span
  class="inline-flex items-center font-medium bg-blue-500 dark:bg-blue-600 text-white dark:text-white border-blue-600 dark:border-blue-500 rounded px-2 py-1 text-sm">
  Rounded
</span>
<span
  class="inline-flex items-center font-medium bg-blue-500 dark:bg-blue-600 text-white dark:text-white border-blue-600 dark:border-blue-500 px-2 py-1 text-sm ml-2">
  Square
</span>
<span
  class="inline-flex items-center font-medium bg-blue-500 dark:bg-blue-600 text-white dark:text-white border-blue-600 dark:border-blue-500 rounded-full px-2 py-1 text-sm ml-2">
  Pill
</span>
---
import { Badge } from '@/components/ui/badge';

---

<Badge shape="rounded">Rounded</Badge>
<Badge shape="square">Square</Badge>
<Badge shape="pill">Pill</Badge>
---
// Imports
import { type HTMLAttributes } from "astro/types";
import { tv, type VariantProps } from "@utils/custom-tv";
import Icon from "./Icon.astro";
import {
  getOutlinedClasses,
  type ColorName,
  getDefaultClasses,
  getSoftClasses,
  colorPalette,
} from "@utils/colorUtils";
import { twMerge } from "tailwind-merge";

// PropTypes for documentation
export const propTypes = {
  content: {
    type: "string | JSX.Element",
    description: "The content of the badge",
  },
  icon: {
    type: "string | { name: string; class?: string }",
    description: "The icon to display in the badge",
  },
  iconPosition: {
    type: ["left", "right"],
    description: "The position of the icon",
    default: "left",
  },
  class: {
    type: "string",
    description: "Additional CSS classes to apply to the badge",
  },
  removable: {
    type: "boolean",
    description: "Whether the badge is removable",
    default: false,
  },
  variant: {
    type: ["solid", "soft", "outline"],
    description: "The variant of the badge",
    default: "solid",
  },
  color: {
    type: Object.keys(colorPalette),
    description: "The color scheme for the badge",
    default: "blue",
  },
  shape: {
    type: ["rounded", "square", "pill"],
    description: "The shape of the badge",
    default: "rounded",
  },
  size: {
    type: ["sm", "md", "lg"],
    description: "The size of the badge",
    default: "md",
  },
  maxWidth: {
    type: "string",
    description: "The maximum width of the badge",
  },
  animated: {
    type: "boolean",
    description: "Whether the badge should have animations",
    default: false,
  },
};

// Types and Interfaces
type BadgeVariants = VariantProps<typeof badgeStyles>;

interface Props extends HTMLAttributes<"span">, BadgeVariants {
  content?: string | JSX.Element;
  icon?: string | { name: string; class?: string };
  iconPosition?: "left" | "right";
  class?: string;
  removable?: boolean;
  variant?: "solid" | "soft" | "outline";
  color?: ColorName;
  shape?: "rounded" | "square" | "pill";
  size?: "sm" | "md" | "lg";
  maxWidth?: string;
  animated?: boolean;
}

// Component Logic
const {
  content,
  icon,
  iconPosition = "left",
  variant = "solid",
  color = "blue",
  removable = false,
  shape = "rounded",
  class: className = "",
  size = "md",
  maxWidth,
  animated = false,
} = Astro.props as Props;

// Styles
const badgeStyles = tv({
  base: "inline-flex items-center font-medium",
  variants: {
    variant: {
      solid: getDefaultClasses(color, { includeHover: false }),
      soft: getSoftClasses(color, { includeHover: false }),
      outline: `${getOutlinedClasses(color, { includeHover: false })} border`,
    },
    shape: {
      rounded: "rounded",
      square: "",
      pill: "rounded-full",
    },
    size: {
      sm: "px-2 py-1 text-xs",
      md: "px-2 py-1 text-sm",
      lg: "px-3 py-1 text-base",
    },
    animated: {
      true: "transition-all duration-300 ease-in-out hover:scale-105",
      false: "",
    },
  },
  defaultVariants: {
    variant: "solid",
    shape: "rounded",
    size: "md",
    animated: false,
  },
});

const finalIcon = typeof icon === "string" ? { name: icon, class: "" } : icon;
---

<span
  class={twMerge(
    badgeStyles({ variant, shape, size, animated }),
    maxWidth && `max-w-[${maxWidth}] truncate`,
    className,
  )}
>
  {
    finalIcon && iconPosition === "left" && (
      <Icon
        name={finalIcon.name}
        class={twMerge(
          `mr-1 ${size === "sm" ? "w-3 h-3" : size === "lg" ? "w-5 h-5" : "w-4 h-4"}`,
          finalIcon.class,
        )}
        aria-hidden="true"
      />
    )
  }
  {content && <Fragment set:html={content} />}
  <slot />
  {
    finalIcon && iconPosition === "right" && (
      <Icon
        name={finalIcon.name}
        class={twMerge(
          `ml-1 ${size === "sm" ? "w-3 h-3" : size === "lg" ? "w-5 h-5" : "w-4 h-4"}`,
          finalIcon.class,
        )}
        aria-hidden="true"
      />
    )
  }
  {
    removable && (
      <button
        type="button"
        class={twMerge(
          "ml-1 -mr-1 hover:bg-opacity-10 hover:bg-black focus:outline-none focus:ring-2 focus:ring-offset-2",
          `focus:ring-${color}-500 rounded-${shape === "pill" ? "full" : "sm"}`,
        )}
        aria-label="Remove"
      >
        <Icon
          name="XMarkIcon"
          class={twMerge(
            size === "sm" ? "w-3 h-3" : size === "lg" ? "w-5 h-5" : "w-4 h-4",
          )}
          aria-hidden="true"
        />
      </button>
    )
  }
</span>

Badge Sizes

Badges with different sizes: small, medium, and large.

 
<span
  class="inline-flex items-center font-medium bg-blue-500 dark:bg-blue-600 text-white dark:text-white border-blue-600 dark:border-blue-500 rounded px-2 py-1 text-xs">
  Small
</span>
<span
  class="inline-flex items-center font-medium bg-blue-500 dark:bg-blue-600 text-white dark:text-white border-blue-600 dark:border-blue-500 rounded px-2 py-1 text-sm ml-2">
  Medium
</span>
<span
  class="inline-flex items-center font-medium bg-blue-500 dark:bg-blue-600 text-white dark:text-white border-blue-600 dark:border-blue-500 rounded px-3 py-1 text-base ml-2">
  Large
</span>
---
import { Badge } from '@/components/ui/badge';

---

<Badge size="sm">Small</Badge>
<Badge size="md">Medium</Badge>
<Badge size="lg">Large</Badge>
---
// Imports
import { type HTMLAttributes } from "astro/types";
import { tv, type VariantProps } from "@utils/custom-tv";
import Icon from "./Icon.astro";
import {
  getOutlinedClasses,
  type ColorName,
  getDefaultClasses,
  getSoftClasses,
  colorPalette,
} from "@utils/colorUtils";
import { twMerge } from "tailwind-merge";

// PropTypes for documentation
export const propTypes = {
  content: {
    type: "string | JSX.Element",
    description: "The content of the badge",
  },
  icon: {
    type: "string | { name: string; class?: string }",
    description: "The icon to display in the badge",
  },
  iconPosition: {
    type: ["left", "right"],
    description: "The position of the icon",
    default: "left",
  },
  class: {
    type: "string",
    description: "Additional CSS classes to apply to the badge",
  },
  removable: {
    type: "boolean",
    description: "Whether the badge is removable",
    default: false,
  },
  variant: {
    type: ["solid", "soft", "outline"],
    description: "The variant of the badge",
    default: "solid",
  },
  color: {
    type: Object.keys(colorPalette),
    description: "The color scheme for the badge",
    default: "blue",
  },
  shape: {
    type: ["rounded", "square", "pill"],
    description: "The shape of the badge",
    default: "rounded",
  },
  size: {
    type: ["sm", "md", "lg"],
    description: "The size of the badge",
    default: "md",
  },
  maxWidth: {
    type: "string",
    description: "The maximum width of the badge",
  },
  animated: {
    type: "boolean",
    description: "Whether the badge should have animations",
    default: false,
  },
};

// Types and Interfaces
type BadgeVariants = VariantProps<typeof badgeStyles>;

interface Props extends HTMLAttributes<"span">, BadgeVariants {
  content?: string | JSX.Element;
  icon?: string | { name: string; class?: string };
  iconPosition?: "left" | "right";
  class?: string;
  removable?: boolean;
  variant?: "solid" | "soft" | "outline";
  color?: ColorName;
  shape?: "rounded" | "square" | "pill";
  size?: "sm" | "md" | "lg";
  maxWidth?: string;
  animated?: boolean;
}

// Component Logic
const {
  content,
  icon,
  iconPosition = "left",
  variant = "solid",
  color = "blue",
  removable = false,
  shape = "rounded",
  class: className = "",
  size = "md",
  maxWidth,
  animated = false,
} = Astro.props as Props;

// Styles
const badgeStyles = tv({
  base: "inline-flex items-center font-medium",
  variants: {
    variant: {
      solid: getDefaultClasses(color, { includeHover: false }),
      soft: getSoftClasses(color, { includeHover: false }),
      outline: `${getOutlinedClasses(color, { includeHover: false })} border`,
    },
    shape: {
      rounded: "rounded",
      square: "",
      pill: "rounded-full",
    },
    size: {
      sm: "px-2 py-1 text-xs",
      md: "px-2 py-1 text-sm",
      lg: "px-3 py-1 text-base",
    },
    animated: {
      true: "transition-all duration-300 ease-in-out hover:scale-105",
      false: "",
    },
  },
  defaultVariants: {
    variant: "solid",
    shape: "rounded",
    size: "md",
    animated: false,
  },
});

const finalIcon = typeof icon === "string" ? { name: icon, class: "" } : icon;
---

<span
  class={twMerge(
    badgeStyles({ variant, shape, size, animated }),
    maxWidth && `max-w-[${maxWidth}] truncate`,
    className,
  )}
>
  {
    finalIcon && iconPosition === "left" && (
      <Icon
        name={finalIcon.name}
        class={twMerge(
          `mr-1 ${size === "sm" ? "w-3 h-3" : size === "lg" ? "w-5 h-5" : "w-4 h-4"}`,
          finalIcon.class,
        )}
        aria-hidden="true"
      />
    )
  }
  {content && <Fragment set:html={content} />}
  <slot />
  {
    finalIcon && iconPosition === "right" && (
      <Icon
        name={finalIcon.name}
        class={twMerge(
          `ml-1 ${size === "sm" ? "w-3 h-3" : size === "lg" ? "w-5 h-5" : "w-4 h-4"}`,
          finalIcon.class,
        )}
        aria-hidden="true"
      />
    )
  }
  {
    removable && (
      <button
        type="button"
        class={twMerge(
          "ml-1 -mr-1 hover:bg-opacity-10 hover:bg-black focus:outline-none focus:ring-2 focus:ring-offset-2",
          `focus:ring-${color}-500 rounded-${shape === "pill" ? "full" : "sm"}`,
        )}
        aria-label="Remove"
      >
        <Icon
          name="XMarkIcon"
          class={twMerge(
            size === "sm" ? "w-3 h-3" : size === "lg" ? "w-5 h-5" : "w-4 h-4",
          )}
          aria-hidden="true"
        />
      </button>
    )
  }
</span>

Badge with Icon

Badges with icons in different positions.

 
<span
  class="inline-flex items-center font-medium bg-blue-500 dark:bg-blue-600 text-white dark:text-white border-blue-600 dark:border-blue-500 rounded px-2 py-1 text-sm">
  <span class="inline-block size-5 mr-1 w-4 h-4">
    <svg
      xmlns="http://www.w3.org/2000/svg"
      viewBox="0 0 24 24"
      fill="currentColor"
      aria-hidden="true"
      data-slot="icon">
      <path
        fill-rule="evenodd"
        d="M2.25 12c0-5.385 4.365-9.75 9.75-9.75s9.75 4.365 9.75 9.75-4.365 9.75-9.75 9.75S2.25 17.385 2.25 12Zm13.36-1.814a.75.75 0 1 0-1.22-.872l-3.236 4.53L9.53 12.22a.75.75 0 0 0-1.06 1.06l2.25 2.25a.75.75 0 0 0 1.14-.094l3.75-5.25Z"
        clip-rule="evenodd"></path>
    </svg>
  </span>
  Left Icon
</span>
<span
  class="inline-flex items-center font-medium bg-blue-500 dark:bg-blue-600 text-white dark:text-white border-blue-600 dark:border-blue-500 rounded px-2 py-1 text-sm ml-2">
  Right Icon
  <span class="inline-block size-5 ml-1 w-4 h-4">
    <svg
      xmlns="http://www.w3.org/2000/svg"
      viewBox="0 0 24 24"
      fill="currentColor"
      aria-hidden="true"
      data-slot="icon">
      <path
        fill-rule="evenodd"
        d="M2.25 12c0-5.385 4.365-9.75 9.75-9.75s9.75 4.365 9.75 9.75-4.365 9.75-9.75 9.75S2.25 17.385 2.25 12Zm13.36-1.814a.75.75 0 1 0-1.22-.872l-3.236 4.53L9.53 12.22a.75.75 0 0 0-1.06 1.06l2.25 2.25a.75.75 0 0 0 1.14-.094l3.75-5.25Z"
        clip-rule="evenodd"></path>
    </svg>
  </span>
</span>
---
import { Badge } from '@/components/ui/badge';

---

<Badge icon="CheckCircleIcon" iconPosition="left">Left Icon</Badge>
<Badge icon="CheckCircleIcon" iconPosition="right">Right Icon</Badge>
---
// Imports
import { type HTMLAttributes } from "astro/types";
import { tv, type VariantProps } from "@utils/custom-tv";
import Icon from "./Icon.astro";
import {
  getOutlinedClasses,
  type ColorName,
  getDefaultClasses,
  getSoftClasses,
  colorPalette,
} from "@utils/colorUtils";
import { twMerge } from "tailwind-merge";

// PropTypes for documentation
export const propTypes = {
  content: {
    type: "string | JSX.Element",
    description: "The content of the badge",
  },
  icon: {
    type: "string | { name: string; class?: string }",
    description: "The icon to display in the badge",
  },
  iconPosition: {
    type: ["left", "right"],
    description: "The position of the icon",
    default: "left",
  },
  class: {
    type: "string",
    description: "Additional CSS classes to apply to the badge",
  },
  removable: {
    type: "boolean",
    description: "Whether the badge is removable",
    default: false,
  },
  variant: {
    type: ["solid", "soft", "outline"],
    description: "The variant of the badge",
    default: "solid",
  },
  color: {
    type: Object.keys(colorPalette),
    description: "The color scheme for the badge",
    default: "blue",
  },
  shape: {
    type: ["rounded", "square", "pill"],
    description: "The shape of the badge",
    default: "rounded",
  },
  size: {
    type: ["sm", "md", "lg"],
    description: "The size of the badge",
    default: "md",
  },
  maxWidth: {
    type: "string",
    description: "The maximum width of the badge",
  },
  animated: {
    type: "boolean",
    description: "Whether the badge should have animations",
    default: false,
  },
};

// Types and Interfaces
type BadgeVariants = VariantProps<typeof badgeStyles>;

interface Props extends HTMLAttributes<"span">, BadgeVariants {
  content?: string | JSX.Element;
  icon?: string | { name: string; class?: string };
  iconPosition?: "left" | "right";
  class?: string;
  removable?: boolean;
  variant?: "solid" | "soft" | "outline";
  color?: ColorName;
  shape?: "rounded" | "square" | "pill";
  size?: "sm" | "md" | "lg";
  maxWidth?: string;
  animated?: boolean;
}

// Component Logic
const {
  content,
  icon,
  iconPosition = "left",
  variant = "solid",
  color = "blue",
  removable = false,
  shape = "rounded",
  class: className = "",
  size = "md",
  maxWidth,
  animated = false,
} = Astro.props as Props;

// Styles
const badgeStyles = tv({
  base: "inline-flex items-center font-medium",
  variants: {
    variant: {
      solid: getDefaultClasses(color, { includeHover: false }),
      soft: getSoftClasses(color, { includeHover: false }),
      outline: `${getOutlinedClasses(color, { includeHover: false })} border`,
    },
    shape: {
      rounded: "rounded",
      square: "",
      pill: "rounded-full",
    },
    size: {
      sm: "px-2 py-1 text-xs",
      md: "px-2 py-1 text-sm",
      lg: "px-3 py-1 text-base",
    },
    animated: {
      true: "transition-all duration-300 ease-in-out hover:scale-105",
      false: "",
    },
  },
  defaultVariants: {
    variant: "solid",
    shape: "rounded",
    size: "md",
    animated: false,
  },
});

const finalIcon = typeof icon === "string" ? { name: icon, class: "" } : icon;
---

<span
  class={twMerge(
    badgeStyles({ variant, shape, size, animated }),
    maxWidth && `max-w-[${maxWidth}] truncate`,
    className,
  )}
>
  {
    finalIcon && iconPosition === "left" && (
      <Icon
        name={finalIcon.name}
        class={twMerge(
          `mr-1 ${size === "sm" ? "w-3 h-3" : size === "lg" ? "w-5 h-5" : "w-4 h-4"}`,
          finalIcon.class,
        )}
        aria-hidden="true"
      />
    )
  }
  {content && <Fragment set:html={content} />}
  <slot />
  {
    finalIcon && iconPosition === "right" && (
      <Icon
        name={finalIcon.name}
        class={twMerge(
          `ml-1 ${size === "sm" ? "w-3 h-3" : size === "lg" ? "w-5 h-5" : "w-4 h-4"}`,
          finalIcon.class,
        )}
        aria-hidden="true"
      />
    )
  }
  {
    removable && (
      <button
        type="button"
        class={twMerge(
          "ml-1 -mr-1 hover:bg-opacity-10 hover:bg-black focus:outline-none focus:ring-2 focus:ring-offset-2",
          `focus:ring-${color}-500 rounded-${shape === "pill" ? "full" : "sm"}`,
        )}
        aria-label="Remove"
      >
        <Icon
          name="XMarkIcon"
          class={twMerge(
            size === "sm" ? "w-3 h-3" : size === "lg" ? "w-5 h-5" : "w-4 h-4",
          )}
          aria-hidden="true"
        />
      </button>
    )
  }
</span>

Removable Badge

A badge that can be removed by the user.

 
<span
  class="inline-flex items-center font-medium bg-blue-500 dark:bg-blue-600 text-white dark:text-white border-blue-600 dark:border-blue-500 rounded px-2 py-1 text-sm">
  <p>Removable</p>
  <button
    type="button"
    class="ml-1 -mr-1 hover:bg-opacity-10 hover:bg-black focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 rounded-sm"
    aria-label="Remove">
    <span class="inline-block size-5 w-4 h-4">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="currentColor"
        aria-hidden="true"
        data-slot="icon">
        <path
          fill-rule="evenodd"
          d="M5.47 5.47a.75.75 0 0 1 1.06 0L12 10.94l5.47-5.47a.75.75 0 1 1 1.06 1.06L13.06 12l5.47 5.47a.75.75 0 1 1-1.06 1.06L12 13.06l-5.47 5.47a.75.75 0 0 1-1.06-1.06L10.94 12 5.47 6.53a.75.75 0 0 1 0-1.06Z"
          clip-rule="evenodd"></path>
      </svg>
    </span>
  </button>
</span>
---
import { Badge } from '@/components/ui/badge';

---

<Badge removable>
Removable
</Badge>
---
// Imports
import { type HTMLAttributes } from "astro/types";
import { tv, type VariantProps } from "@utils/custom-tv";
import Icon from "./Icon.astro";
import {
  getOutlinedClasses,
  type ColorName,
  getDefaultClasses,
  getSoftClasses,
  colorPalette,
} from "@utils/colorUtils";
import { twMerge } from "tailwind-merge";

// PropTypes for documentation
export const propTypes = {
  content: {
    type: "string | JSX.Element",
    description: "The content of the badge",
  },
  icon: {
    type: "string | { name: string; class?: string }",
    description: "The icon to display in the badge",
  },
  iconPosition: {
    type: ["left", "right"],
    description: "The position of the icon",
    default: "left",
  },
  class: {
    type: "string",
    description: "Additional CSS classes to apply to the badge",
  },
  removable: {
    type: "boolean",
    description: "Whether the badge is removable",
    default: false,
  },
  variant: {
    type: ["solid", "soft", "outline"],
    description: "The variant of the badge",
    default: "solid",
  },
  color: {
    type: Object.keys(colorPalette),
    description: "The color scheme for the badge",
    default: "blue",
  },
  shape: {
    type: ["rounded", "square", "pill"],
    description: "The shape of the badge",
    default: "rounded",
  },
  size: {
    type: ["sm", "md", "lg"],
    description: "The size of the badge",
    default: "md",
  },
  maxWidth: {
    type: "string",
    description: "The maximum width of the badge",
  },
  animated: {
    type: "boolean",
    description: "Whether the badge should have animations",
    default: false,
  },
};

// Types and Interfaces
type BadgeVariants = VariantProps<typeof badgeStyles>;

interface Props extends HTMLAttributes<"span">, BadgeVariants {
  content?: string | JSX.Element;
  icon?: string | { name: string; class?: string };
  iconPosition?: "left" | "right";
  class?: string;
  removable?: boolean;
  variant?: "solid" | "soft" | "outline";
  color?: ColorName;
  shape?: "rounded" | "square" | "pill";
  size?: "sm" | "md" | "lg";
  maxWidth?: string;
  animated?: boolean;
}

// Component Logic
const {
  content,
  icon,
  iconPosition = "left",
  variant = "solid",
  color = "blue",
  removable = false,
  shape = "rounded",
  class: className = "",
  size = "md",
  maxWidth,
  animated = false,
} = Astro.props as Props;

// Styles
const badgeStyles = tv({
  base: "inline-flex items-center font-medium",
  variants: {
    variant: {
      solid: getDefaultClasses(color, { includeHover: false }),
      soft: getSoftClasses(color, { includeHover: false }),
      outline: `${getOutlinedClasses(color, { includeHover: false })} border`,
    },
    shape: {
      rounded: "rounded",
      square: "",
      pill: "rounded-full",
    },
    size: {
      sm: "px-2 py-1 text-xs",
      md: "px-2 py-1 text-sm",
      lg: "px-3 py-1 text-base",
    },
    animated: {
      true: "transition-all duration-300 ease-in-out hover:scale-105",
      false: "",
    },
  },
  defaultVariants: {
    variant: "solid",
    shape: "rounded",
    size: "md",
    animated: false,
  },
});

const finalIcon = typeof icon === "string" ? { name: icon, class: "" } : icon;
---

<span
  class={twMerge(
    badgeStyles({ variant, shape, size, animated }),
    maxWidth && `max-w-[${maxWidth}] truncate`,
    className,
  )}
>
  {
    finalIcon && iconPosition === "left" && (
      <Icon
        name={finalIcon.name}
        class={twMerge(
          `mr-1 ${size === "sm" ? "w-3 h-3" : size === "lg" ? "w-5 h-5" : "w-4 h-4"}`,
          finalIcon.class,
        )}
        aria-hidden="true"
      />
    )
  }
  {content && <Fragment set:html={content} />}
  <slot />
  {
    finalIcon && iconPosition === "right" && (
      <Icon
        name={finalIcon.name}
        class={twMerge(
          `ml-1 ${size === "sm" ? "w-3 h-3" : size === "lg" ? "w-5 h-5" : "w-4 h-4"}`,
          finalIcon.class,
        )}
        aria-hidden="true"
      />
    )
  }
  {
    removable && (
      <button
        type="button"
        class={twMerge(
          "ml-1 -mr-1 hover:bg-opacity-10 hover:bg-black focus:outline-none focus:ring-2 focus:ring-offset-2",
          `focus:ring-${color}-500 rounded-${shape === "pill" ? "full" : "sm"}`,
        )}
        aria-label="Remove"
      >
        <Icon
          name="XMarkIcon"
          class={twMerge(
            size === "sm" ? "w-3 h-3" : size === "lg" ? "w-5 h-5" : "w-4 h-4",
          )}
          aria-hidden="true"
        />
      </button>
    )
  }
</span>

Animated Badge

A badge with hover animation.

 
<span
  class="inline-flex items-center font-medium bg-blue-500 dark:bg-blue-600 text-white dark:text-white border-blue-600 dark:border-blue-500 rounded px-2 py-1 text-sm transition-all duration-300 ease-in-out hover:scale-105">
  <p>Animated</p>
</span>
---
import { Badge } from '@/components/ui/badge';

---

<Badge animated>
Animated
</Badge>
---
// Imports
import { type HTMLAttributes } from "astro/types";
import { tv, type VariantProps } from "@utils/custom-tv";
import Icon from "./Icon.astro";
import {
  getOutlinedClasses,
  type ColorName,
  getDefaultClasses,
  getSoftClasses,
  colorPalette,
} from "@utils/colorUtils";
import { twMerge } from "tailwind-merge";

// PropTypes for documentation
export const propTypes = {
  content: {
    type: "string | JSX.Element",
    description: "The content of the badge",
  },
  icon: {
    type: "string | { name: string; class?: string }",
    description: "The icon to display in the badge",
  },
  iconPosition: {
    type: ["left", "right"],
    description: "The position of the icon",
    default: "left",
  },
  class: {
    type: "string",
    description: "Additional CSS classes to apply to the badge",
  },
  removable: {
    type: "boolean",
    description: "Whether the badge is removable",
    default: false,
  },
  variant: {
    type: ["solid", "soft", "outline"],
    description: "The variant of the badge",
    default: "solid",
  },
  color: {
    type: Object.keys(colorPalette),
    description: "The color scheme for the badge",
    default: "blue",
  },
  shape: {
    type: ["rounded", "square", "pill"],
    description: "The shape of the badge",
    default: "rounded",
  },
  size: {
    type: ["sm", "md", "lg"],
    description: "The size of the badge",
    default: "md",
  },
  maxWidth: {
    type: "string",
    description: "The maximum width of the badge",
  },
  animated: {
    type: "boolean",
    description: "Whether the badge should have animations",
    default: false,
  },
};

// Types and Interfaces
type BadgeVariants = VariantProps<typeof badgeStyles>;

interface Props extends HTMLAttributes<"span">, BadgeVariants {
  content?: string | JSX.Element;
  icon?: string | { name: string; class?: string };
  iconPosition?: "left" | "right";
  class?: string;
  removable?: boolean;
  variant?: "solid" | "soft" | "outline";
  color?: ColorName;
  shape?: "rounded" | "square" | "pill";
  size?: "sm" | "md" | "lg";
  maxWidth?: string;
  animated?: boolean;
}

// Component Logic
const {
  content,
  icon,
  iconPosition = "left",
  variant = "solid",
  color = "blue",
  removable = false,
  shape = "rounded",
  class: className = "",
  size = "md",
  maxWidth,
  animated = false,
} = Astro.props as Props;

// Styles
const badgeStyles = tv({
  base: "inline-flex items-center font-medium",
  variants: {
    variant: {
      solid: getDefaultClasses(color, { includeHover: false }),
      soft: getSoftClasses(color, { includeHover: false }),
      outline: `${getOutlinedClasses(color, { includeHover: false })} border`,
    },
    shape: {
      rounded: "rounded",
      square: "",
      pill: "rounded-full",
    },
    size: {
      sm: "px-2 py-1 text-xs",
      md: "px-2 py-1 text-sm",
      lg: "px-3 py-1 text-base",
    },
    animated: {
      true: "transition-all duration-300 ease-in-out hover:scale-105",
      false: "",
    },
  },
  defaultVariants: {
    variant: "solid",
    shape: "rounded",
    size: "md",
    animated: false,
  },
});

const finalIcon = typeof icon === "string" ? { name: icon, class: "" } : icon;
---

<span
  class={twMerge(
    badgeStyles({ variant, shape, size, animated }),
    maxWidth && `max-w-[${maxWidth}] truncate`,
    className,
  )}
>
  {
    finalIcon && iconPosition === "left" && (
      <Icon
        name={finalIcon.name}
        class={twMerge(
          `mr-1 ${size === "sm" ? "w-3 h-3" : size === "lg" ? "w-5 h-5" : "w-4 h-4"}`,
          finalIcon.class,
        )}
        aria-hidden="true"
      />
    )
  }
  {content && <Fragment set:html={content} />}
  <slot />
  {
    finalIcon && iconPosition === "right" && (
      <Icon
        name={finalIcon.name}
        class={twMerge(
          `ml-1 ${size === "sm" ? "w-3 h-3" : size === "lg" ? "w-5 h-5" : "w-4 h-4"}`,
          finalIcon.class,
        )}
        aria-hidden="true"
      />
    )
  }
  {
    removable && (
      <button
        type="button"
        class={twMerge(
          "ml-1 -mr-1 hover:bg-opacity-10 hover:bg-black focus:outline-none focus:ring-2 focus:ring-offset-2",
          `focus:ring-${color}-500 rounded-${shape === "pill" ? "full" : "sm"}`,
        )}
        aria-label="Remove"
      >
        <Icon
          name="XMarkIcon"
          class={twMerge(
            size === "sm" ? "w-3 h-3" : size === "lg" ? "w-5 h-5" : "w-4 h-4",
          )}
          aria-hidden="true"
        />
      </button>
    )
  }
</span>

Badge with Max Width

A badge with a maximum width and text truncation.

 
<span
  class="inline-flex items-center font-medium bg-blue-500 dark:bg-blue-600 text-white dark:text-white border-blue-600 dark:border-blue-500 rounded px-2 py-1 text-sm max-w-[100px] truncate">
  <p>This is a very long badge text that will be truncated</p>
</span>
---
import { Badge } from '@/components/ui/badge';

---

<Badge maxWidth="100px">
This is a very long badge text that will be truncated
</Badge>
---
// Imports
import { type HTMLAttributes } from "astro/types";
import { tv, type VariantProps } from "@utils/custom-tv";
import Icon from "./Icon.astro";
import {
  getOutlinedClasses,
  type ColorName,
  getDefaultClasses,
  getSoftClasses,
  colorPalette,
} from "@utils/colorUtils";
import { twMerge } from "tailwind-merge";

// PropTypes for documentation
export const propTypes = {
  content: {
    type: "string | JSX.Element",
    description: "The content of the badge",
  },
  icon: {
    type: "string | { name: string; class?: string }",
    description: "The icon to display in the badge",
  },
  iconPosition: {
    type: ["left", "right"],
    description: "The position of the icon",
    default: "left",
  },
  class: {
    type: "string",
    description: "Additional CSS classes to apply to the badge",
  },
  removable: {
    type: "boolean",
    description: "Whether the badge is removable",
    default: false,
  },
  variant: {
    type: ["solid", "soft", "outline"],
    description: "The variant of the badge",
    default: "solid",
  },
  color: {
    type: Object.keys(colorPalette),
    description: "The color scheme for the badge",
    default: "blue",
  },
  shape: {
    type: ["rounded", "square", "pill"],
    description: "The shape of the badge",
    default: "rounded",
  },
  size: {
    type: ["sm", "md", "lg"],
    description: "The size of the badge",
    default: "md",
  },
  maxWidth: {
    type: "string",
    description: "The maximum width of the badge",
  },
  animated: {
    type: "boolean",
    description: "Whether the badge should have animations",
    default: false,
  },
};

// Types and Interfaces
type BadgeVariants = VariantProps<typeof badgeStyles>;

interface Props extends HTMLAttributes<"span">, BadgeVariants {
  content?: string | JSX.Element;
  icon?: string | { name: string; class?: string };
  iconPosition?: "left" | "right";
  class?: string;
  removable?: boolean;
  variant?: "solid" | "soft" | "outline";
  color?: ColorName;
  shape?: "rounded" | "square" | "pill";
  size?: "sm" | "md" | "lg";
  maxWidth?: string;
  animated?: boolean;
}

// Component Logic
const {
  content,
  icon,
  iconPosition = "left",
  variant = "solid",
  color = "blue",
  removable = false,
  shape = "rounded",
  class: className = "",
  size = "md",
  maxWidth,
  animated = false,
} = Astro.props as Props;

// Styles
const badgeStyles = tv({
  base: "inline-flex items-center font-medium",
  variants: {
    variant: {
      solid: getDefaultClasses(color, { includeHover: false }),
      soft: getSoftClasses(color, { includeHover: false }),
      outline: `${getOutlinedClasses(color, { includeHover: false })} border`,
    },
    shape: {
      rounded: "rounded",
      square: "",
      pill: "rounded-full",
    },
    size: {
      sm: "px-2 py-1 text-xs",
      md: "px-2 py-1 text-sm",
      lg: "px-3 py-1 text-base",
    },
    animated: {
      true: "transition-all duration-300 ease-in-out hover:scale-105",
      false: "",
    },
  },
  defaultVariants: {
    variant: "solid",
    shape: "rounded",
    size: "md",
    animated: false,
  },
});

const finalIcon = typeof icon === "string" ? { name: icon, class: "" } : icon;
---

<span
  class={twMerge(
    badgeStyles({ variant, shape, size, animated }),
    maxWidth && `max-w-[${maxWidth}] truncate`,
    className,
  )}
>
  {
    finalIcon && iconPosition === "left" && (
      <Icon
        name={finalIcon.name}
        class={twMerge(
          `mr-1 ${size === "sm" ? "w-3 h-3" : size === "lg" ? "w-5 h-5" : "w-4 h-4"}`,
          finalIcon.class,
        )}
        aria-hidden="true"
      />
    )
  }
  {content && <Fragment set:html={content} />}
  <slot />
  {
    finalIcon && iconPosition === "right" && (
      <Icon
        name={finalIcon.name}
        class={twMerge(
          `ml-1 ${size === "sm" ? "w-3 h-3" : size === "lg" ? "w-5 h-5" : "w-4 h-4"}`,
          finalIcon.class,
        )}
        aria-hidden="true"
      />
    )
  }
  {
    removable && (
      <button
        type="button"
        class={twMerge(
          "ml-1 -mr-1 hover:bg-opacity-10 hover:bg-black focus:outline-none focus:ring-2 focus:ring-offset-2",
          `focus:ring-${color}-500 rounded-${shape === "pill" ? "full" : "sm"}`,
        )}
        aria-label="Remove"
      >
        <Icon
          name="XMarkIcon"
          class={twMerge(
            size === "sm" ? "w-3 h-3" : size === "lg" ? "w-5 h-5" : "w-4 h-4",
          )}
          aria-hidden="true"
        />
      </button>
    )
  }
</span>

Badge Colors

Badges with different color options.

 
<span
  class="inline-flex items-center font-medium bg-slate-500 dark:bg-slate-600 text-white dark:text-white border-slate-600 dark:border-slate-500 rounded px-2 py-1 text-sm">
  Slate
</span>
<span
  class="inline-flex items-center font-medium bg-gray-500 dark:bg-gray-600 text-white dark:text-white border-gray-600 dark:border-gray-500 rounded px-2 py-1 text-sm ml-2">
  Gray
</span>
<span
  class="inline-flex items-center font-medium bg-zinc-500 dark:bg-zinc-600 text-white dark:text-white border-zinc-600 dark:border-zinc-500 rounded px-2 py-1 text-sm ml-2">
  Zinc
</span>
<span
  class="inline-flex items-center font-medium bg-neutral-500 dark:bg-neutral-600 text-white dark:text-white border-neutral-600 dark:border-neutral-500 rounded px-2 py-1 text-sm ml-2">
  Neutral
</span>
<span
  class="inline-flex items-center font-medium bg-stone-500 dark:bg-stone-600 text-white dark:text-white border-stone-600 dark:border-stone-500 rounded px-2 py-1 text-sm ml-2">
  Stone
</span>
<span
  class="inline-flex items-center font-medium bg-red-500 dark:bg-red-600 text-white dark:text-white border-red-600 dark:border-red-500 rounded px-2 py-1 text-sm ml-2">
  Red
</span>
<span
  class="inline-flex items-center font-medium bg-orange-500 dark:bg-orange-600 text-white dark:text-white border-orange-600 dark:border-orange-500 rounded px-2 py-1 text-sm ml-2">
  Orange
</span>
<span
  class="inline-flex items-center font-medium bg-amber-500 dark:bg-amber-600 text-white dark:text-white border-amber-600 dark:border-amber-500 rounded px-2 py-1 text-sm ml-2">
  Amber
</span>
<span
  class="inline-flex items-center font-medium bg-yellow-500 dark:bg-yellow-600 text-white dark:text-white border-yellow-600 dark:border-yellow-500 rounded px-2 py-1 text-sm ml-2">
  Yellow
</span>
<span
  class="inline-flex items-center font-medium bg-lime-500 dark:bg-lime-600 text-white dark:text-white border-lime-600 dark:border-lime-500 rounded px-2 py-1 text-sm ml-2">
  Lime
</span>
<span
  class="inline-flex items-center font-medium bg-green-500 dark:bg-green-600 text-white dark:text-white border-green-600 dark:border-green-500 rounded px-2 py-1 text-sm ml-2">
  Green
</span>
<span
  class="inline-flex items-center font-medium bg-emerald-500 dark:bg-emerald-600 text-white dark:text-white border-emerald-600 dark:border-emerald-500 rounded px-2 py-1 text-sm ml-2">
  Emerald
</span>
<span
  class="inline-flex items-center font-medium bg-teal-500 dark:bg-teal-600 text-white dark:text-white border-teal-600 dark:border-teal-500 rounded px-2 py-1 text-sm ml-2">
  Teal
</span>
<span
  class="inline-flex items-center font-medium bg-cyan-500 dark:bg-cyan-600 text-white dark:text-white border-cyan-600 dark:border-cyan-500 rounded px-2 py-1 text-sm ml-2">
  Cyan
</span>
<span
  class="inline-flex items-center font-medium bg-sky-500 dark:bg-sky-600 text-white dark:text-white border-sky-600 dark:border-sky-500 rounded px-2 py-1 text-sm ml-2">
  Sky
</span>
<span
  class="inline-flex items-center font-medium bg-blue-500 dark:bg-blue-600 text-white dark:text-white border-blue-600 dark:border-blue-500 rounded px-2 py-1 text-sm ml-2">
  Blue
</span>
<span
  class="inline-flex items-center font-medium bg-indigo-500 dark:bg-indigo-600 text-white dark:text-white border-indigo-600 dark:border-indigo-500 rounded px-2 py-1 text-sm ml-2">
  Indigo
</span>
<span
  class="inline-flex items-center font-medium bg-violet-500 dark:bg-violet-600 text-white dark:text-white border-violet-600 dark:border-violet-500 rounded px-2 py-1 text-sm ml-2">
  Violet
</span>
<span
  class="inline-flex items-center font-medium bg-purple-500 dark:bg-purple-600 text-white dark:text-white border-purple-600 dark:border-purple-500 rounded px-2 py-1 text-sm ml-2">
  Purple
</span>
<span
  class="inline-flex items-center font-medium bg-fuchsia-500 dark:bg-fuchsia-600 text-white dark:text-white border-fuchsia-600 dark:border-fuchsia-500 rounded px-2 py-1 text-sm ml-2">
  Fuchsia
</span>
<span
  class="inline-flex items-center font-medium bg-pink-500 dark:bg-pink-600 text-white dark:text-white border-pink-600 dark:border-pink-500 rounded px-2 py-1 text-sm ml-2">
  Pink
</span>
<span
  class="inline-flex items-center font-medium bg-rose-500 dark:bg-rose-600 text-white dark:text-white border-rose-600 dark:border-rose-500 rounded px-2 py-1 text-sm ml-2">
  Rose
</span>
---
import { Badge } from '@/components/ui/badge';

---

<Badge color="slate">Slate</Badge>
<Badge color="gray">Gray</Badge>
<Badge color="zinc">Zinc</Badge>
<Badge color="neutral">Neutral</Badge>
<Badge color="stone">Stone</Badge>
<Badge color="red">Red</Badge>
<Badge color="orange">Orange</Badge>
<Badge color="amber">Amber</Badge>
<Badge color="yellow">Yellow</Badge>
<Badge color="lime">Lime</Badge>
<Badge color="green">Green</Badge>
<Badge color="emerald">Emerald</Badge>
<Badge color="teal">Teal</Badge>
<Badge color="cyan">Cyan</Badge>
<Badge color="sky">Sky</Badge>
<Badge color="blue">Blue</Badge>
<Badge color="indigo">Indigo</Badge>
<Badge color="violet">Violet</Badge>
<Badge color="purple">Purple</Badge>
<Badge color="fuchsia">Fuchsia</Badge>
<Badge color="pink">Pink</Badge>
<Badge color="rose">Rose</Badge>
---
// Imports
import { type HTMLAttributes } from "astro/types";
import { tv, type VariantProps } from "@utils/custom-tv";
import Icon from "./Icon.astro";
import {
  getOutlinedClasses,
  type ColorName,
  getDefaultClasses,
  getSoftClasses,
  colorPalette,
} from "@utils/colorUtils";
import { twMerge } from "tailwind-merge";

// PropTypes for documentation
export const propTypes = {
  content: {
    type: "string | JSX.Element",
    description: "The content of the badge",
  },
  icon: {
    type: "string | { name: string; class?: string }",
    description: "The icon to display in the badge",
  },
  iconPosition: {
    type: ["left", "right"],
    description: "The position of the icon",
    default: "left",
  },
  class: {
    type: "string",
    description: "Additional CSS classes to apply to the badge",
  },
  removable: {
    type: "boolean",
    description: "Whether the badge is removable",
    default: false,
  },
  variant: {
    type: ["solid", "soft", "outline"],
    description: "The variant of the badge",
    default: "solid",
  },
  color: {
    type: Object.keys(colorPalette),
    description: "The color scheme for the badge",
    default: "blue",
  },
  shape: {
    type: ["rounded", "square", "pill"],
    description: "The shape of the badge",
    default: "rounded",
  },
  size: {
    type: ["sm", "md", "lg"],
    description: "The size of the badge",
    default: "md",
  },
  maxWidth: {
    type: "string",
    description: "The maximum width of the badge",
  },
  animated: {
    type: "boolean",
    description: "Whether the badge should have animations",
    default: false,
  },
};

// Types and Interfaces
type BadgeVariants = VariantProps<typeof badgeStyles>;

interface Props extends HTMLAttributes<"span">, BadgeVariants {
  content?: string | JSX.Element;
  icon?: string | { name: string; class?: string };
  iconPosition?: "left" | "right";
  class?: string;
  removable?: boolean;
  variant?: "solid" | "soft" | "outline";
  color?: ColorName;
  shape?: "rounded" | "square" | "pill";
  size?: "sm" | "md" | "lg";
  maxWidth?: string;
  animated?: boolean;
}

// Component Logic
const {
  content,
  icon,
  iconPosition = "left",
  variant = "solid",
  color = "blue",
  removable = false,
  shape = "rounded",
  class: className = "",
  size = "md",
  maxWidth,
  animated = false,
} = Astro.props as Props;

// Styles
const badgeStyles = tv({
  base: "inline-flex items-center font-medium",
  variants: {
    variant: {
      solid: getDefaultClasses(color, { includeHover: false }),
      soft: getSoftClasses(color, { includeHover: false }),
      outline: `${getOutlinedClasses(color, { includeHover: false })} border`,
    },
    shape: {
      rounded: "rounded",
      square: "",
      pill: "rounded-full",
    },
    size: {
      sm: "px-2 py-1 text-xs",
      md: "px-2 py-1 text-sm",
      lg: "px-3 py-1 text-base",
    },
    animated: {
      true: "transition-all duration-300 ease-in-out hover:scale-105",
      false: "",
    },
  },
  defaultVariants: {
    variant: "solid",
    shape: "rounded",
    size: "md",
    animated: false,
  },
});

const finalIcon = typeof icon === "string" ? { name: icon, class: "" } : icon;
---

<span
  class={twMerge(
    badgeStyles({ variant, shape, size, animated }),
    maxWidth && `max-w-[${maxWidth}] truncate`,
    className,
  )}
>
  {
    finalIcon && iconPosition === "left" && (
      <Icon
        name={finalIcon.name}
        class={twMerge(
          `mr-1 ${size === "sm" ? "w-3 h-3" : size === "lg" ? "w-5 h-5" : "w-4 h-4"}`,
          finalIcon.class,
        )}
        aria-hidden="true"
      />
    )
  }
  {content && <Fragment set:html={content} />}
  <slot />
  {
    finalIcon && iconPosition === "right" && (
      <Icon
        name={finalIcon.name}
        class={twMerge(
          `ml-1 ${size === "sm" ? "w-3 h-3" : size === "lg" ? "w-5 h-5" : "w-4 h-4"}`,
          finalIcon.class,
        )}
        aria-hidden="true"
      />
    )
  }
  {
    removable && (
      <button
        type="button"
        class={twMerge(
          "ml-1 -mr-1 hover:bg-opacity-10 hover:bg-black focus:outline-none focus:ring-2 focus:ring-offset-2",
          `focus:ring-${color}-500 rounded-${shape === "pill" ? "full" : "sm"}`,
        )}
        aria-label="Remove"
      >
        <Icon
          name="XMarkIcon"
          class={twMerge(
            size === "sm" ? "w-3 h-3" : size === "lg" ? "w-5 h-5" : "w-4 h-4",
          )}
          aria-hidden="true"
        />
      </button>
    )
  }
</span>

Combined Badge Features

A badge combining multiple features.

 
<span
  class="inline-flex items-center font-medium bg-green-100 dark:bg-green-800 text-green-700 dark:text-green-100 border-green-200 dark:border-green-600 rounded-full px-3 py-1 text-base transition-all duration-300 ease-in-out hover:scale-105">
  <span class="inline-block size-5 mr-1 w-5 h-5">
    <svg
      xmlns="http://www.w3.org/2000/svg"
      viewBox="0 0 24 24"
      fill="currentColor"
      aria-hidden="true"
      data-slot="icon">
      <path
        fill-rule="evenodd"
        d="M2.25 12c0-5.385 4.365-9.75 9.75-9.75s9.75 4.365 9.75 9.75-4.365 9.75-9.75 9.75S2.25 17.385 2.25 12Zm13.36-1.814a.75.75 0 1 0-1.22-.872l-3.236 4.53L9.53 12.22a.75.75 0 0 0-1.06 1.06l2.25 2.25a.75.75 0 0 0 1.14-.094l3.75-5.25Z"
        clip-rule="evenodd"></path>
    </svg>
  </span>
  <p>Success</p>
  <button
    type="button"
    class="ml-1 -mr-1 hover:bg-opacity-10 hover:bg-black focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500 rounded-full"
    aria-label="Remove">
    <span class="inline-block size-5 w-5 h-5">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="currentColor"
        aria-hidden="true"
        data-slot="icon">
        <path
          fill-rule="evenodd"
          d="M5.47 5.47a.75.75 0 0 1 1.06 0L12 10.94l5.47-5.47a.75.75 0 1 1 1.06 1.06L13.06 12l5.47 5.47a.75.75 0 1 1-1.06 1.06L12 13.06l-5.47 5.47a.75.75 0 0 1-1.06-1.06L10.94 12 5.47 6.53a.75.75 0 0 1 0-1.06Z"
          clip-rule="evenodd"></path>
      </svg>
    </span>
  </button>
</span>
---
import { Badge } from '@/components/ui/badge';

---

<Badge
variant="soft"
color="green"
shape="pill"
size="lg"
icon="CheckCircleIcon"
iconPosition="left"
removable
animated
>
Success
</Badge>
---
// Imports
import { type HTMLAttributes } from "astro/types";
import { tv, type VariantProps } from "@utils/custom-tv";
import Icon from "./Icon.astro";
import {
  getOutlinedClasses,
  type ColorName,
  getDefaultClasses,
  getSoftClasses,
  colorPalette,
} from "@utils/colorUtils";
import { twMerge } from "tailwind-merge";

// PropTypes for documentation
export const propTypes = {
  content: {
    type: "string | JSX.Element",
    description: "The content of the badge",
  },
  icon: {
    type: "string | { name: string; class?: string }",
    description: "The icon to display in the badge",
  },
  iconPosition: {
    type: ["left", "right"],
    description: "The position of the icon",
    default: "left",
  },
  class: {
    type: "string",
    description: "Additional CSS classes to apply to the badge",
  },
  removable: {
    type: "boolean",
    description: "Whether the badge is removable",
    default: false,
  },
  variant: {
    type: ["solid", "soft", "outline"],
    description: "The variant of the badge",
    default: "solid",
  },
  color: {
    type: Object.keys(colorPalette),
    description: "The color scheme for the badge",
    default: "blue",
  },
  shape: {
    type: ["rounded", "square", "pill"],
    description: "The shape of the badge",
    default: "rounded",
  },
  size: {
    type: ["sm", "md", "lg"],
    description: "The size of the badge",
    default: "md",
  },
  maxWidth: {
    type: "string",
    description: "The maximum width of the badge",
  },
  animated: {
    type: "boolean",
    description: "Whether the badge should have animations",
    default: false,
  },
};

// Types and Interfaces
type BadgeVariants = VariantProps<typeof badgeStyles>;

interface Props extends HTMLAttributes<"span">, BadgeVariants {
  content?: string | JSX.Element;
  icon?: string | { name: string; class?: string };
  iconPosition?: "left" | "right";
  class?: string;
  removable?: boolean;
  variant?: "solid" | "soft" | "outline";
  color?: ColorName;
  shape?: "rounded" | "square" | "pill";
  size?: "sm" | "md" | "lg";
  maxWidth?: string;
  animated?: boolean;
}

// Component Logic
const {
  content,
  icon,
  iconPosition = "left",
  variant = "solid",
  color = "blue",
  removable = false,
  shape = "rounded",
  class: className = "",
  size = "md",
  maxWidth,
  animated = false,
} = Astro.props as Props;

// Styles
const badgeStyles = tv({
  base: "inline-flex items-center font-medium",
  variants: {
    variant: {
      solid: getDefaultClasses(color, { includeHover: false }),
      soft: getSoftClasses(color, { includeHover: false }),
      outline: `${getOutlinedClasses(color, { includeHover: false })} border`,
    },
    shape: {
      rounded: "rounded",
      square: "",
      pill: "rounded-full",
    },
    size: {
      sm: "px-2 py-1 text-xs",
      md: "px-2 py-1 text-sm",
      lg: "px-3 py-1 text-base",
    },
    animated: {
      true: "transition-all duration-300 ease-in-out hover:scale-105",
      false: "",
    },
  },
  defaultVariants: {
    variant: "solid",
    shape: "rounded",
    size: "md",
    animated: false,
  },
});

const finalIcon = typeof icon === "string" ? { name: icon, class: "" } : icon;
---

<span
  class={twMerge(
    badgeStyles({ variant, shape, size, animated }),
    maxWidth && `max-w-[${maxWidth}] truncate`,
    className,
  )}
>
  {
    finalIcon && iconPosition === "left" && (
      <Icon
        name={finalIcon.name}
        class={twMerge(
          `mr-1 ${size === "sm" ? "w-3 h-3" : size === "lg" ? "w-5 h-5" : "w-4 h-4"}`,
          finalIcon.class,
        )}
        aria-hidden="true"
      />
    )
  }
  {content && <Fragment set:html={content} />}
  <slot />
  {
    finalIcon && iconPosition === "right" && (
      <Icon
        name={finalIcon.name}
        class={twMerge(
          `ml-1 ${size === "sm" ? "w-3 h-3" : size === "lg" ? "w-5 h-5" : "w-4 h-4"}`,
          finalIcon.class,
        )}
        aria-hidden="true"
      />
    )
  }
  {
    removable && (
      <button
        type="button"
        class={twMerge(
          "ml-1 -mr-1 hover:bg-opacity-10 hover:bg-black focus:outline-none focus:ring-2 focus:ring-offset-2",
          `focus:ring-${color}-500 rounded-${shape === "pill" ? "full" : "sm"}`,
        )}
        aria-label="Remove"
      >
        <Icon
          name="XMarkIcon"
          class={twMerge(
            size === "sm" ? "w-3 h-3" : size === "lg" ? "w-5 h-5" : "w-4 h-4",
          )}
          aria-hidden="true"
        />
      </button>
    )
  }
</span>

Component Properties

Property Type Default Description
content string | JSX.Element - The content of the badge
icon string | { name: string; class?: string } - The icon to display in the badge
iconPosition left | right "left" The position of the icon
class string - Additional CSS classes to apply to the badge
removable boolean false Whether the badge is removable
variant solid | soft | outline "solid" The variant of the badge
color white | slate | gray | zinc | neutral | stone | red | orange | amber | yellow | lime | green | emerald | teal | cyan | sky | blue | indigo | violet | purple | fuchsia | pink | rose "blue" The color scheme for the badge
shape rounded | square | pill "rounded" The shape of the badge
size sm | md | lg "md" The size of the badge
maxWidth string - The maximum width of the badge
animated boolean false Whether the badge should have animations