Buttons

Versatile button components with various styles, sizes, and customization options. Supports different variants, colors, shapes, and icon integrations. previews

Default Button

A basic button component with default settings.

 
<button
  class="inline-flex items-center justify-center font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 bg-blue-500 hover:bg-blue-600 dark:bg-blue-600 dark:hover:bg-blue-500 text-white dark:text-white hover:text-blue-100 dark:hover:text-blue-200 px-3 py-2 text-sm rounded-md">
  Default Button
</button>
---
import { Button } from '@/components/ui/button';

---

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

// PropTypes for documentation
export const propTypes = {
  variant: {
    type: ["solid", "outline", "ghost", "soft", "text"],
    description: "The variant of the button",
    default: "solid",
  },
  size: {
    type: ["xs", "sm", "md", "lg", "xl", "2xl"],
    description: "The size of the button",
    default: "md",
  },
  color: {
    type: Object.keys(colorPalette),
    description: "The color scheme for the button",
    default: "blue",
  },
  shape: {
    type: ["square", "rounded", "pill"],
    description: "The shape of the button",
    default: "rounded",
  },
  fullWidth: {
    type: "boolean",
    description: "Whether the button should take full width",
    default: false,
  },
  icon: {
    type: "string | { name: string; class?: string }",
    description: "The icon to display in the button",
  },
  rightIcon: {
    type: "string | { name: string; class?: string }",
    description: "The icon to display on the right side of the button",
  },
  iconOnly: {
    type: "boolean",
    description: "Whether the button should only display an icon",
    default: false,
  },
  disabled: {
    type: "boolean",
    description: "Whether the button is disabled",
    default: false,
  },
  class: {
    type: "string",
    description: "Additional CSS classes to apply to the button",
  },
  as: {
    type: ["button", "a"],
    description: "The HTML element to render the button as",
    default: "button",
  },
  href: {
    type: "string",
    description: "The URL to link to when using an anchor tag",
  },
};

// Types and Interfaces
type ButtonVariants = VariantProps<typeof buttonStyles>;

interface Props extends HTMLAttributes<"button" | "a">, ButtonVariants {
  variant?: "solid" | "outline" | "ghost" | "soft" | "text";
  size?: "xs" | "sm" | "md" | "lg" | "xl" | "2xl";
  color?: ColorName;
  shape?: "square" | "rounded" | "pill";
  fullWidth?: boolean;
  icon?: string | { name: string; class?: string };
  rightIcon?: string | { name: string; class?: string };
  iconOnly?: boolean;
  disabled?: boolean;
  class?: string;
  as?: "button" | "a";
  href?: string;
}

// Component Logic
const {
  variant = "solid",
  size = "md",
  color = "blue",
  shape = "rounded",
  fullWidth = false,
  icon,
  rightIcon,
  iconOnly = false,
  disabled = false,
  class: className = "",
  as = "button",
  href,
  ...rest
} = Astro.props as Props;

const normalizeIcon = (icon: Props["icon"]) =>
  typeof icon === "string" ? { name: icon, class: "" } : icon;

const finalIcon = icon ? normalizeIcon(icon) : null;
const finalRightIcon = rightIcon ? normalizeIcon(rightIcon) : null;

// Styles
const buttonStyles = tv({
  base: "inline-flex items-center justify-center font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2",
  variants: {
    variant: {
      solid: getButtonClasses(color),
      outline: `${getOutlinedClasses(color)} border`,
      ghost: `${getGhostClasses(color)}`,
      soft: getSoftClasses(color),
      text: getSoftTextClass(color) + " hover:opacity-70",
    },
    size: {
      xs: "px-2 py-1 text-xs",
      sm: "px-2 py-1 text-sm",
      md: "px-3 py-2 text-sm",
      lg: "px-4 py-2 text-base",
      xl: "px-5 py-3 text-lg",
      "2xl": "px-6 py-4 text-xl",
    },
    shape: {
      square: "",
      rounded: "rounded-md",
      pill: "rounded-full",
    },
    fullWidth: {
      true: "w-full",
      false: "",
    },
    iconOnly: {
      true: "p-2",
      false: "",
    },
    disabled: {
      true: "opacity-50 cursor-not-allowed",
      false: "",
    },
  },

  defaultVariants: {
    variant: "solid",
    size: "md",
    shape: "rounded",
    fullWidth: false,
    iconOnly: false,
    disabled: false,
  },
});

const iconSize =
  size === "xs" || size === "sm"
    ? "w-4 h-4"
    : size === "2xl"
      ? "w-6 h-6"
      : "w-5 h-5";
const Element = as;
---

<Element
  class={twMerge(
    buttonStyles({
      variant,
      size,
      color,
      shape,
      fullWidth,
      iconOnly,
      disabled,
    }),
    className,
  )}
  disabled={as === "button" ? disabled : undefined}
  aria-disabled={disabled}
  href={as === "a" ? href : undefined}
  {...rest}
>
  {
    finalIcon && (
      <Icon
        name={finalIcon.name}
        class={twMerge(iconSize, !iconOnly && "mr-2", finalIcon.class)}
        aria-hidden="true"
      />
    )
  }
  {!iconOnly && <slot />}
  {
    finalRightIcon && (
      <Icon
        name={finalRightIcon.name}
        class={twMerge(iconSize, "ml-2", finalRightIcon.class)}
        aria-hidden="true"
      />
    )
  }
</Element>

Button Variants

Different button variants: solid, outline, ghost, soft, and text.

 
<div class="space-x-2">
  <button
    class="inline-flex items-center justify-center font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 bg-blue-500 hover:bg-blue-600 dark:bg-blue-600 dark:hover:bg-blue-500 text-white dark:text-white hover:text-blue-100 dark:hover:text-blue-200 px-3 py-2 text-sm rounded-md">
    Solid
  </button>
  <button
    class="inline-flex items-center justify-center font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 bg-transparent text-blue-500 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-200 border-blue-500 dark:border-blue-400 hover:border-blue-700 dark:hover:border-blue-200 border px-3 py-2 text-sm rounded-md">
    Outline
  </button>
  <button
    class="inline-flex items-center justify-center font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 bg-transparent text-blue-500 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-200 hover:bg-blue-200 dark:hover:bg-blue-700 px-3 py-2 text-sm rounded-md">
    Ghost
  </button>
  <button
    class="inline-flex items-center justify-center font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 bg-blue-100 dark:bg-blue-800 hover:bg-blue-200 dark:hover:bg-blue-700 text-blue-700 dark:text-blue-100 border-blue-200 dark:border-blue-600 px-3 py-2 text-sm rounded-md">
    Soft
  </button>
  <button
    class="inline-flex items-center justify-center font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 text-blue-700 dark:text-blue-100 hover:opacity-70 px-3 py-2 text-sm rounded-md">
    Text
  </button>
</div>
---
import { Button } from '@/components/ui/button';

---

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

// PropTypes for documentation
export const propTypes = {
  variant: {
    type: ["solid", "outline", "ghost", "soft", "text"],
    description: "The variant of the button",
    default: "solid",
  },
  size: {
    type: ["xs", "sm", "md", "lg", "xl", "2xl"],
    description: "The size of the button",
    default: "md",
  },
  color: {
    type: Object.keys(colorPalette),
    description: "The color scheme for the button",
    default: "blue",
  },
  shape: {
    type: ["square", "rounded", "pill"],
    description: "The shape of the button",
    default: "rounded",
  },
  fullWidth: {
    type: "boolean",
    description: "Whether the button should take full width",
    default: false,
  },
  icon: {
    type: "string | { name: string; class?: string }",
    description: "The icon to display in the button",
  },
  rightIcon: {
    type: "string | { name: string; class?: string }",
    description: "The icon to display on the right side of the button",
  },
  iconOnly: {
    type: "boolean",
    description: "Whether the button should only display an icon",
    default: false,
  },
  disabled: {
    type: "boolean",
    description: "Whether the button is disabled",
    default: false,
  },
  class: {
    type: "string",
    description: "Additional CSS classes to apply to the button",
  },
  as: {
    type: ["button", "a"],
    description: "The HTML element to render the button as",
    default: "button",
  },
  href: {
    type: "string",
    description: "The URL to link to when using an anchor tag",
  },
};

// Types and Interfaces
type ButtonVariants = VariantProps<typeof buttonStyles>;

interface Props extends HTMLAttributes<"button" | "a">, ButtonVariants {
  variant?: "solid" | "outline" | "ghost" | "soft" | "text";
  size?: "xs" | "sm" | "md" | "lg" | "xl" | "2xl";
  color?: ColorName;
  shape?: "square" | "rounded" | "pill";
  fullWidth?: boolean;
  icon?: string | { name: string; class?: string };
  rightIcon?: string | { name: string; class?: string };
  iconOnly?: boolean;
  disabled?: boolean;
  class?: string;
  as?: "button" | "a";
  href?: string;
}

// Component Logic
const {
  variant = "solid",
  size = "md",
  color = "blue",
  shape = "rounded",
  fullWidth = false,
  icon,
  rightIcon,
  iconOnly = false,
  disabled = false,
  class: className = "",
  as = "button",
  href,
  ...rest
} = Astro.props as Props;

const normalizeIcon = (icon: Props["icon"]) =>
  typeof icon === "string" ? { name: icon, class: "" } : icon;

const finalIcon = icon ? normalizeIcon(icon) : null;
const finalRightIcon = rightIcon ? normalizeIcon(rightIcon) : null;

// Styles
const buttonStyles = tv({
  base: "inline-flex items-center justify-center font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2",
  variants: {
    variant: {
      solid: getButtonClasses(color),
      outline: `${getOutlinedClasses(color)} border`,
      ghost: `${getGhostClasses(color)}`,
      soft: getSoftClasses(color),
      text: getSoftTextClass(color) + " hover:opacity-70",
    },
    size: {
      xs: "px-2 py-1 text-xs",
      sm: "px-2 py-1 text-sm",
      md: "px-3 py-2 text-sm",
      lg: "px-4 py-2 text-base",
      xl: "px-5 py-3 text-lg",
      "2xl": "px-6 py-4 text-xl",
    },
    shape: {
      square: "",
      rounded: "rounded-md",
      pill: "rounded-full",
    },
    fullWidth: {
      true: "w-full",
      false: "",
    },
    iconOnly: {
      true: "p-2",
      false: "",
    },
    disabled: {
      true: "opacity-50 cursor-not-allowed",
      false: "",
    },
  },

  defaultVariants: {
    variant: "solid",
    size: "md",
    shape: "rounded",
    fullWidth: false,
    iconOnly: false,
    disabled: false,
  },
});

const iconSize =
  size === "xs" || size === "sm"
    ? "w-4 h-4"
    : size === "2xl"
      ? "w-6 h-6"
      : "w-5 h-5";
const Element = as;
---

<Element
  class={twMerge(
    buttonStyles({
      variant,
      size,
      color,
      shape,
      fullWidth,
      iconOnly,
      disabled,
    }),
    className,
  )}
  disabled={as === "button" ? disabled : undefined}
  aria-disabled={disabled}
  href={as === "a" ? href : undefined}
  {...rest}
>
  {
    finalIcon && (
      <Icon
        name={finalIcon.name}
        class={twMerge(iconSize, !iconOnly && "mr-2", finalIcon.class)}
        aria-hidden="true"
      />
    )
  }
  {!iconOnly && <slot />}
  {
    finalRightIcon && (
      <Icon
        name={finalRightIcon.name}
        class={twMerge(iconSize, "ml-2", finalRightIcon.class)}
        aria-hidden="true"
      />
    )
  }
</Element>

Button Sizes

Buttons in different sizes: xs, sm, md, lg, xl, and 2xl.

 
<div class="block space-x-6">
  <button
    class="inline-flex items-center justify-center font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 bg-blue-500 hover:bg-blue-600 dark:bg-blue-600 dark:hover:bg-blue-500 text-white dark:text-white hover:text-blue-100 dark:hover:text-blue-200 px-2 py-1 text-xs rounded-md">
    Extra Small
  </button>
  <button
    class="inline-flex items-center justify-center font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 bg-blue-500 hover:bg-blue-600 dark:bg-blue-600 dark:hover:bg-blue-500 text-white dark:text-white hover:text-blue-100 dark:hover:text-blue-200 px-2 py-1 text-sm rounded-md">
    Small
  </button>
  <button
    class="inline-flex items-center justify-center font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 bg-blue-500 hover:bg-blue-600 dark:bg-blue-600 dark:hover:bg-blue-500 text-white dark:text-white hover:text-blue-100 dark:hover:text-blue-200 px-3 py-2 text-sm rounded-md">
    Medium
  </button>
  <button
    class="inline-flex items-center justify-center font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 bg-blue-500 hover:bg-blue-600 dark:bg-blue-600 dark:hover:bg-blue-500 text-white dark:text-white hover:text-blue-100 dark:hover:text-blue-200 px-4 py-2 text-base rounded-md">
    Large
  </button>
  <button
    class="inline-flex items-center justify-center font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 bg-blue-500 hover:bg-blue-600 dark:bg-blue-600 dark:hover:bg-blue-500 text-white dark:text-white hover:text-blue-100 dark:hover:text-blue-200 px-5 py-3 text-lg rounded-md">
    Extra Large
  </button>
  <button
    class="inline-flex items-center justify-center font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 bg-blue-500 hover:bg-blue-600 dark:bg-blue-600 dark:hover:bg-blue-500 text-white dark:text-white hover:text-blue-100 dark:hover:text-blue-200 px-6 py-4 text-xl rounded-md">
    2X Large
  </button>
</div>
---
import { Button } from '@/components/ui/button';

---

<Button size="xs">Extra Small</Button>
<Button size="sm">Small</Button>
<Button size="md">Medium</Button>
<Button size="lg">Large</Button>
<Button size="xl">Extra Large</Button>
<Button size="2xl">2X Large</Button>
---
// Imports
import { type HTMLAttributes } from "astro/types";
import { tv, type VariantProps } from "@utils/custom-tv";
import Icon from "./Icon.astro";
import {
  getDefaultClasses,
  getOutlinedClasses,
  getSoftClasses,
  type ColorName,
  colorPalette,
  getHardTextClass,
  getSoftTextClass,
  getGhostClasses,
  getButtonClasses,
} from "@utils/colorUtils";
import { twMerge } from "tailwind-merge";

// PropTypes for documentation
export const propTypes = {
  variant: {
    type: ["solid", "outline", "ghost", "soft", "text"],
    description: "The variant of the button",
    default: "solid",
  },
  size: {
    type: ["xs", "sm", "md", "lg", "xl", "2xl"],
    description: "The size of the button",
    default: "md",
  },
  color: {
    type: Object.keys(colorPalette),
    description: "The color scheme for the button",
    default: "blue",
  },
  shape: {
    type: ["square", "rounded", "pill"],
    description: "The shape of the button",
    default: "rounded",
  },
  fullWidth: {
    type: "boolean",
    description: "Whether the button should take full width",
    default: false,
  },
  icon: {
    type: "string | { name: string; class?: string }",
    description: "The icon to display in the button",
  },
  rightIcon: {
    type: "string | { name: string; class?: string }",
    description: "The icon to display on the right side of the button",
  },
  iconOnly: {
    type: "boolean",
    description: "Whether the button should only display an icon",
    default: false,
  },
  disabled: {
    type: "boolean",
    description: "Whether the button is disabled",
    default: false,
  },
  class: {
    type: "string",
    description: "Additional CSS classes to apply to the button",
  },
  as: {
    type: ["button", "a"],
    description: "The HTML element to render the button as",
    default: "button",
  },
  href: {
    type: "string",
    description: "The URL to link to when using an anchor tag",
  },
};

// Types and Interfaces
type ButtonVariants = VariantProps<typeof buttonStyles>;

interface Props extends HTMLAttributes<"button" | "a">, ButtonVariants {
  variant?: "solid" | "outline" | "ghost" | "soft" | "text";
  size?: "xs" | "sm" | "md" | "lg" | "xl" | "2xl";
  color?: ColorName;
  shape?: "square" | "rounded" | "pill";
  fullWidth?: boolean;
  icon?: string | { name: string; class?: string };
  rightIcon?: string | { name: string; class?: string };
  iconOnly?: boolean;
  disabled?: boolean;
  class?: string;
  as?: "button" | "a";
  href?: string;
}

// Component Logic
const {
  variant = "solid",
  size = "md",
  color = "blue",
  shape = "rounded",
  fullWidth = false,
  icon,
  rightIcon,
  iconOnly = false,
  disabled = false,
  class: className = "",
  as = "button",
  href,
  ...rest
} = Astro.props as Props;

const normalizeIcon = (icon: Props["icon"]) =>
  typeof icon === "string" ? { name: icon, class: "" } : icon;

const finalIcon = icon ? normalizeIcon(icon) : null;
const finalRightIcon = rightIcon ? normalizeIcon(rightIcon) : null;

// Styles
const buttonStyles = tv({
  base: "inline-flex items-center justify-center font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2",
  variants: {
    variant: {
      solid: getButtonClasses(color),
      outline: `${getOutlinedClasses(color)} border`,
      ghost: `${getGhostClasses(color)}`,
      soft: getSoftClasses(color),
      text: getSoftTextClass(color) + " hover:opacity-70",
    },
    size: {
      xs: "px-2 py-1 text-xs",
      sm: "px-2 py-1 text-sm",
      md: "px-3 py-2 text-sm",
      lg: "px-4 py-2 text-base",
      xl: "px-5 py-3 text-lg",
      "2xl": "px-6 py-4 text-xl",
    },
    shape: {
      square: "",
      rounded: "rounded-md",
      pill: "rounded-full",
    },
    fullWidth: {
      true: "w-full",
      false: "",
    },
    iconOnly: {
      true: "p-2",
      false: "",
    },
    disabled: {
      true: "opacity-50 cursor-not-allowed",
      false: "",
    },
  },

  defaultVariants: {
    variant: "solid",
    size: "md",
    shape: "rounded",
    fullWidth: false,
    iconOnly: false,
    disabled: false,
  },
});

const iconSize =
  size === "xs" || size === "sm"
    ? "w-4 h-4"
    : size === "2xl"
      ? "w-6 h-6"
      : "w-5 h-5";
const Element = as;
---

<Element
  class={twMerge(
    buttonStyles({
      variant,
      size,
      color,
      shape,
      fullWidth,
      iconOnly,
      disabled,
    }),
    className,
  )}
  disabled={as === "button" ? disabled : undefined}
  aria-disabled={disabled}
  href={as === "a" ? href : undefined}
  {...rest}
>
  {
    finalIcon && (
      <Icon
        name={finalIcon.name}
        class={twMerge(iconSize, !iconOnly && "mr-2", finalIcon.class)}
        aria-hidden="true"
      />
    )
  }
  {!iconOnly && <slot />}
  {
    finalRightIcon && (
      <Icon
        name={finalRightIcon.name}
        class={twMerge(iconSize, "ml-2", finalRightIcon.class)}
        aria-hidden="true"
      />
    )
  }
</Element>

Button Colors

Buttons with different color options.

 
<div class="space-x-2">
  <button
    class="inline-flex items-center justify-center font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 bg-blue-500 hover:bg-blue-600 dark:bg-blue-600 dark:hover:bg-blue-500 text-white dark:text-white hover:text-blue-100 dark:hover:text-blue-200 px-3 py-2 text-sm rounded-md">
    Blue
  </button>
  <button
    class="inline-flex items-center justify-center font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 bg-red-500 hover:bg-red-600 dark:bg-red-600 dark:hover:bg-red-500 text-white dark:text-white hover:text-red-100 dark:hover:text-red-200 px-3 py-2 text-sm rounded-md">
    Red
  </button>
  <button
    class="inline-flex items-center justify-center font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 bg-green-500 hover:bg-green-600 dark:bg-green-600 dark:hover:bg-green-500 text-white dark:text-white hover:text-green-100 dark:hover:text-green-200 px-3 py-2 text-sm rounded-md">
    Green
  </button>
  <button
    class="inline-flex items-center justify-center font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 bg-yellow-500 hover:bg-yellow-600 dark:bg-yellow-600 dark:hover:bg-yellow-500 text-white dark:text-white hover:text-yellow-100 dark:hover:text-yellow-200 px-3 py-2 text-sm rounded-md">
    Yellow
  </button>
  <button
    class="inline-flex items-center justify-center font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 bg-purple-500 hover:bg-purple-600 dark:bg-purple-600 dark:hover:bg-purple-500 text-white dark:text-white hover:text-purple-100 dark:hover:text-purple-200 px-3 py-2 text-sm rounded-md">
    Purple
  </button>
  <button
    class="inline-flex items-center justify-center font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 bg-pink-500 hover:bg-pink-600 dark:bg-pink-600 dark:hover:bg-pink-500 text-white dark:text-white hover:text-pink-100 dark:hover:text-pink-200 px-3 py-2 text-sm rounded-md">
    Pink
  </button>
</div>
---
import { Button } from '@/components/ui/button';

---

<Button color="blue">Blue</Button>
<Button color="red">Red</Button>
<Button color="green">Green</Button>
<Button color="yellow">Yellow</Button>
<Button color="purple">Purple</Button>
<Button color="pink">Pink</Button>
---
// Imports
import { type HTMLAttributes } from "astro/types";
import { tv, type VariantProps } from "@utils/custom-tv";
import Icon from "./Icon.astro";
import {
  getDefaultClasses,
  getOutlinedClasses,
  getSoftClasses,
  type ColorName,
  colorPalette,
  getHardTextClass,
  getSoftTextClass,
  getGhostClasses,
  getButtonClasses,
} from "@utils/colorUtils";
import { twMerge } from "tailwind-merge";

// PropTypes for documentation
export const propTypes = {
  variant: {
    type: ["solid", "outline", "ghost", "soft", "text"],
    description: "The variant of the button",
    default: "solid",
  },
  size: {
    type: ["xs", "sm", "md", "lg", "xl", "2xl"],
    description: "The size of the button",
    default: "md",
  },
  color: {
    type: Object.keys(colorPalette),
    description: "The color scheme for the button",
    default: "blue",
  },
  shape: {
    type: ["square", "rounded", "pill"],
    description: "The shape of the button",
    default: "rounded",
  },
  fullWidth: {
    type: "boolean",
    description: "Whether the button should take full width",
    default: false,
  },
  icon: {
    type: "string | { name: string; class?: string }",
    description: "The icon to display in the button",
  },
  rightIcon: {
    type: "string | { name: string; class?: string }",
    description: "The icon to display on the right side of the button",
  },
  iconOnly: {
    type: "boolean",
    description: "Whether the button should only display an icon",
    default: false,
  },
  disabled: {
    type: "boolean",
    description: "Whether the button is disabled",
    default: false,
  },
  class: {
    type: "string",
    description: "Additional CSS classes to apply to the button",
  },
  as: {
    type: ["button", "a"],
    description: "The HTML element to render the button as",
    default: "button",
  },
  href: {
    type: "string",
    description: "The URL to link to when using an anchor tag",
  },
};

// Types and Interfaces
type ButtonVariants = VariantProps<typeof buttonStyles>;

interface Props extends HTMLAttributes<"button" | "a">, ButtonVariants {
  variant?: "solid" | "outline" | "ghost" | "soft" | "text";
  size?: "xs" | "sm" | "md" | "lg" | "xl" | "2xl";
  color?: ColorName;
  shape?: "square" | "rounded" | "pill";
  fullWidth?: boolean;
  icon?: string | { name: string; class?: string };
  rightIcon?: string | { name: string; class?: string };
  iconOnly?: boolean;
  disabled?: boolean;
  class?: string;
  as?: "button" | "a";
  href?: string;
}

// Component Logic
const {
  variant = "solid",
  size = "md",
  color = "blue",
  shape = "rounded",
  fullWidth = false,
  icon,
  rightIcon,
  iconOnly = false,
  disabled = false,
  class: className = "",
  as = "button",
  href,
  ...rest
} = Astro.props as Props;

const normalizeIcon = (icon: Props["icon"]) =>
  typeof icon === "string" ? { name: icon, class: "" } : icon;

const finalIcon = icon ? normalizeIcon(icon) : null;
const finalRightIcon = rightIcon ? normalizeIcon(rightIcon) : null;

// Styles
const buttonStyles = tv({
  base: "inline-flex items-center justify-center font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2",
  variants: {
    variant: {
      solid: getButtonClasses(color),
      outline: `${getOutlinedClasses(color)} border`,
      ghost: `${getGhostClasses(color)}`,
      soft: getSoftClasses(color),
      text: getSoftTextClass(color) + " hover:opacity-70",
    },
    size: {
      xs: "px-2 py-1 text-xs",
      sm: "px-2 py-1 text-sm",
      md: "px-3 py-2 text-sm",
      lg: "px-4 py-2 text-base",
      xl: "px-5 py-3 text-lg",
      "2xl": "px-6 py-4 text-xl",
    },
    shape: {
      square: "",
      rounded: "rounded-md",
      pill: "rounded-full",
    },
    fullWidth: {
      true: "w-full",
      false: "",
    },
    iconOnly: {
      true: "p-2",
      false: "",
    },
    disabled: {
      true: "opacity-50 cursor-not-allowed",
      false: "",
    },
  },

  defaultVariants: {
    variant: "solid",
    size: "md",
    shape: "rounded",
    fullWidth: false,
    iconOnly: false,
    disabled: false,
  },
});

const iconSize =
  size === "xs" || size === "sm"
    ? "w-4 h-4"
    : size === "2xl"
      ? "w-6 h-6"
      : "w-5 h-5";
const Element = as;
---

<Element
  class={twMerge(
    buttonStyles({
      variant,
      size,
      color,
      shape,
      fullWidth,
      iconOnly,
      disabled,
    }),
    className,
  )}
  disabled={as === "button" ? disabled : undefined}
  aria-disabled={disabled}
  href={as === "a" ? href : undefined}
  {...rest}
>
  {
    finalIcon && (
      <Icon
        name={finalIcon.name}
        class={twMerge(iconSize, !iconOnly && "mr-2", finalIcon.class)}
        aria-hidden="true"
      />
    )
  }
  {!iconOnly && <slot />}
  {
    finalRightIcon && (
      <Icon
        name={finalRightIcon.name}
        class={twMerge(iconSize, "ml-2", finalRightIcon.class)}
        aria-hidden="true"
      />
    )
  }
</Element>

Button Shapes

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

 
<div class="space-x-2">
  <button
    class="inline-flex items-center justify-center font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 bg-blue-500 hover:bg-blue-600 dark:bg-blue-600 dark:hover:bg-blue-500 text-white dark:text-white hover:text-blue-100 dark:hover:text-blue-200 px-3 py-2 text-sm">
    Square
  </button>
  <button
    class="inline-flex items-center justify-center font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 bg-blue-500 hover:bg-blue-600 dark:bg-blue-600 dark:hover:bg-blue-500 text-white dark:text-white hover:text-blue-100 dark:hover:text-blue-200 px-3 py-2 text-sm rounded-md">
    Rounded
  </button>
  <button
    class="inline-flex items-center justify-center font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 bg-blue-500 hover:bg-blue-600 dark:bg-blue-600 dark:hover:bg-blue-500 text-white dark:text-white hover:text-blue-100 dark:hover:text-blue-200 px-3 py-2 text-sm rounded-full">
    Pill
  </button>
</div>
---
import { Button } from '@/components/ui/button';

---

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

// PropTypes for documentation
export const propTypes = {
  variant: {
    type: ["solid", "outline", "ghost", "soft", "text"],
    description: "The variant of the button",
    default: "solid",
  },
  size: {
    type: ["xs", "sm", "md", "lg", "xl", "2xl"],
    description: "The size of the button",
    default: "md",
  },
  color: {
    type: Object.keys(colorPalette),
    description: "The color scheme for the button",
    default: "blue",
  },
  shape: {
    type: ["square", "rounded", "pill"],
    description: "The shape of the button",
    default: "rounded",
  },
  fullWidth: {
    type: "boolean",
    description: "Whether the button should take full width",
    default: false,
  },
  icon: {
    type: "string | { name: string; class?: string }",
    description: "The icon to display in the button",
  },
  rightIcon: {
    type: "string | { name: string; class?: string }",
    description: "The icon to display on the right side of the button",
  },
  iconOnly: {
    type: "boolean",
    description: "Whether the button should only display an icon",
    default: false,
  },
  disabled: {
    type: "boolean",
    description: "Whether the button is disabled",
    default: false,
  },
  class: {
    type: "string",
    description: "Additional CSS classes to apply to the button",
  },
  as: {
    type: ["button", "a"],
    description: "The HTML element to render the button as",
    default: "button",
  },
  href: {
    type: "string",
    description: "The URL to link to when using an anchor tag",
  },
};

// Types and Interfaces
type ButtonVariants = VariantProps<typeof buttonStyles>;

interface Props extends HTMLAttributes<"button" | "a">, ButtonVariants {
  variant?: "solid" | "outline" | "ghost" | "soft" | "text";
  size?: "xs" | "sm" | "md" | "lg" | "xl" | "2xl";
  color?: ColorName;
  shape?: "square" | "rounded" | "pill";
  fullWidth?: boolean;
  icon?: string | { name: string; class?: string };
  rightIcon?: string | { name: string; class?: string };
  iconOnly?: boolean;
  disabled?: boolean;
  class?: string;
  as?: "button" | "a";
  href?: string;
}

// Component Logic
const {
  variant = "solid",
  size = "md",
  color = "blue",
  shape = "rounded",
  fullWidth = false,
  icon,
  rightIcon,
  iconOnly = false,
  disabled = false,
  class: className = "",
  as = "button",
  href,
  ...rest
} = Astro.props as Props;

const normalizeIcon = (icon: Props["icon"]) =>
  typeof icon === "string" ? { name: icon, class: "" } : icon;

const finalIcon = icon ? normalizeIcon(icon) : null;
const finalRightIcon = rightIcon ? normalizeIcon(rightIcon) : null;

// Styles
const buttonStyles = tv({
  base: "inline-flex items-center justify-center font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2",
  variants: {
    variant: {
      solid: getButtonClasses(color),
      outline: `${getOutlinedClasses(color)} border`,
      ghost: `${getGhostClasses(color)}`,
      soft: getSoftClasses(color),
      text: getSoftTextClass(color) + " hover:opacity-70",
    },
    size: {
      xs: "px-2 py-1 text-xs",
      sm: "px-2 py-1 text-sm",
      md: "px-3 py-2 text-sm",
      lg: "px-4 py-2 text-base",
      xl: "px-5 py-3 text-lg",
      "2xl": "px-6 py-4 text-xl",
    },
    shape: {
      square: "",
      rounded: "rounded-md",
      pill: "rounded-full",
    },
    fullWidth: {
      true: "w-full",
      false: "",
    },
    iconOnly: {
      true: "p-2",
      false: "",
    },
    disabled: {
      true: "opacity-50 cursor-not-allowed",
      false: "",
    },
  },

  defaultVariants: {
    variant: "solid",
    size: "md",
    shape: "rounded",
    fullWidth: false,
    iconOnly: false,
    disabled: false,
  },
});

const iconSize =
  size === "xs" || size === "sm"
    ? "w-4 h-4"
    : size === "2xl"
      ? "w-6 h-6"
      : "w-5 h-5";
const Element = as;
---

<Element
  class={twMerge(
    buttonStyles({
      variant,
      size,
      color,
      shape,
      fullWidth,
      iconOnly,
      disabled,
    }),
    className,
  )}
  disabled={as === "button" ? disabled : undefined}
  aria-disabled={disabled}
  href={as === "a" ? href : undefined}
  {...rest}
>
  {
    finalIcon && (
      <Icon
        name={finalIcon.name}
        class={twMerge(iconSize, !iconOnly && "mr-2", finalIcon.class)}
        aria-hidden="true"
      />
    )
  }
  {!iconOnly && <slot />}
  {
    finalRightIcon && (
      <Icon
        name={finalRightIcon.name}
        class={twMerge(iconSize, "ml-2", finalRightIcon.class)}
        aria-hidden="true"
      />
    )
  }
</Element>

Full Width Button

A button that takes up the full width of its container.

 
<button
  class="inline-flex items-center justify-center font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 bg-blue-500 hover:bg-blue-600 dark:bg-blue-600 dark:hover:bg-blue-500 text-white dark:text-white hover:text-blue-100 dark:hover:text-blue-200 px-3 py-2 text-sm rounded-md w-full">
  Full Width Button
</button>
---
import { Button } from '@/components/ui/button';

---

<Button fullWidth>Full Width Button</Button>
---
// Imports
import { type HTMLAttributes } from "astro/types";
import { tv, type VariantProps } from "@utils/custom-tv";
import Icon from "./Icon.astro";
import {
  getDefaultClasses,
  getOutlinedClasses,
  getSoftClasses,
  type ColorName,
  colorPalette,
  getHardTextClass,
  getSoftTextClass,
  getGhostClasses,
  getButtonClasses,
} from "@utils/colorUtils";
import { twMerge } from "tailwind-merge";

// PropTypes for documentation
export const propTypes = {
  variant: {
    type: ["solid", "outline", "ghost", "soft", "text"],
    description: "The variant of the button",
    default: "solid",
  },
  size: {
    type: ["xs", "sm", "md", "lg", "xl", "2xl"],
    description: "The size of the button",
    default: "md",
  },
  color: {
    type: Object.keys(colorPalette),
    description: "The color scheme for the button",
    default: "blue",
  },
  shape: {
    type: ["square", "rounded", "pill"],
    description: "The shape of the button",
    default: "rounded",
  },
  fullWidth: {
    type: "boolean",
    description: "Whether the button should take full width",
    default: false,
  },
  icon: {
    type: "string | { name: string; class?: string }",
    description: "The icon to display in the button",
  },
  rightIcon: {
    type: "string | { name: string; class?: string }",
    description: "The icon to display on the right side of the button",
  },
  iconOnly: {
    type: "boolean",
    description: "Whether the button should only display an icon",
    default: false,
  },
  disabled: {
    type: "boolean",
    description: "Whether the button is disabled",
    default: false,
  },
  class: {
    type: "string",
    description: "Additional CSS classes to apply to the button",
  },
  as: {
    type: ["button", "a"],
    description: "The HTML element to render the button as",
    default: "button",
  },
  href: {
    type: "string",
    description: "The URL to link to when using an anchor tag",
  },
};

// Types and Interfaces
type ButtonVariants = VariantProps<typeof buttonStyles>;

interface Props extends HTMLAttributes<"button" | "a">, ButtonVariants {
  variant?: "solid" | "outline" | "ghost" | "soft" | "text";
  size?: "xs" | "sm" | "md" | "lg" | "xl" | "2xl";
  color?: ColorName;
  shape?: "square" | "rounded" | "pill";
  fullWidth?: boolean;
  icon?: string | { name: string; class?: string };
  rightIcon?: string | { name: string; class?: string };
  iconOnly?: boolean;
  disabled?: boolean;
  class?: string;
  as?: "button" | "a";
  href?: string;
}

// Component Logic
const {
  variant = "solid",
  size = "md",
  color = "blue",
  shape = "rounded",
  fullWidth = false,
  icon,
  rightIcon,
  iconOnly = false,
  disabled = false,
  class: className = "",
  as = "button",
  href,
  ...rest
} = Astro.props as Props;

const normalizeIcon = (icon: Props["icon"]) =>
  typeof icon === "string" ? { name: icon, class: "" } : icon;

const finalIcon = icon ? normalizeIcon(icon) : null;
const finalRightIcon = rightIcon ? normalizeIcon(rightIcon) : null;

// Styles
const buttonStyles = tv({
  base: "inline-flex items-center justify-center font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2",
  variants: {
    variant: {
      solid: getButtonClasses(color),
      outline: `${getOutlinedClasses(color)} border`,
      ghost: `${getGhostClasses(color)}`,
      soft: getSoftClasses(color),
      text: getSoftTextClass(color) + " hover:opacity-70",
    },
    size: {
      xs: "px-2 py-1 text-xs",
      sm: "px-2 py-1 text-sm",
      md: "px-3 py-2 text-sm",
      lg: "px-4 py-2 text-base",
      xl: "px-5 py-3 text-lg",
      "2xl": "px-6 py-4 text-xl",
    },
    shape: {
      square: "",
      rounded: "rounded-md",
      pill: "rounded-full",
    },
    fullWidth: {
      true: "w-full",
      false: "",
    },
    iconOnly: {
      true: "p-2",
      false: "",
    },
    disabled: {
      true: "opacity-50 cursor-not-allowed",
      false: "",
    },
  },

  defaultVariants: {
    variant: "solid",
    size: "md",
    shape: "rounded",
    fullWidth: false,
    iconOnly: false,
    disabled: false,
  },
});

const iconSize =
  size === "xs" || size === "sm"
    ? "w-4 h-4"
    : size === "2xl"
      ? "w-6 h-6"
      : "w-5 h-5";
const Element = as;
---

<Element
  class={twMerge(
    buttonStyles({
      variant,
      size,
      color,
      shape,
      fullWidth,
      iconOnly,
      disabled,
    }),
    className,
  )}
  disabled={as === "button" ? disabled : undefined}
  aria-disabled={disabled}
  href={as === "a" ? href : undefined}
  {...rest}
>
  {
    finalIcon && (
      <Icon
        name={finalIcon.name}
        class={twMerge(iconSize, !iconOnly && "mr-2", finalIcon.class)}
        aria-hidden="true"
      />
    )
  }
  {!iconOnly && <slot />}
  {
    finalRightIcon && (
      <Icon
        name={finalRightIcon.name}
        class={twMerge(iconSize, "ml-2", finalRightIcon.class)}
        aria-hidden="true"
      />
    )
  }
</Element>

Buttons with Icons

Buttons with left icon, right icon, and icon-only options.

 
<div class="space-x-2 flex items-center">
  <button
    class="inline-flex items-center justify-center font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 bg-blue-500 hover:bg-blue-600 dark:bg-blue-600 dark:hover:bg-blue-500 text-white dark:text-white hover:text-blue-100 dark:hover:text-blue-200 px-3 py-2 text-sm rounded-md">
    <span class="inline-block size-5 w-5 h-5 mr-2">
      <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="M7.5 6a4.5 4.5 0 1 1 9 0 4.5 4.5 0 0 1-9 0ZM3.751 20.105a8.25 8.25 0 0 1 16.498 0 .75.75 0 0 1-.437.695A18.683 18.683 0 0 1 12 22.5c-2.786 0-5.433-.608-7.812-1.7a.75.75 0 0 1-.437-.695Z"
          clip-rule="evenodd"></path>
      </svg>
    </span>
    Left Icon
  </button>
  <button
    class="inline-flex items-center justify-center font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 bg-blue-500 hover:bg-blue-600 dark:bg-blue-600 dark:hover:bg-blue-500 text-white dark:text-white hover:text-blue-100 dark:hover:text-blue-200 px-3 py-2 text-sm rounded-md">
    Right Icon
    <span class="inline-block size-5 w-5 h-5 ml-2">
      <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="M12.97 3.97a.75.75 0 0 1 1.06 0l7.5 7.5a.75.75 0 0 1 0 1.06l-7.5 7.5a.75.75 0 1 1-1.06-1.06l6.22-6.22H3a.75.75 0 0 1 0-1.5h16.19l-6.22-6.22a.75.75 0 0 1 0-1.06Z"
          clip-rule="evenodd"></path>
      </svg>
    </span>
  </button>
  <button
    class="inline-flex items-center justify-center font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 bg-blue-500 hover:bg-blue-600 dark:bg-blue-600 dark:hover:bg-blue-500 text-white dark:text-white hover:text-blue-100 dark:hover:text-blue-200 text-sm rounded-md p-2"
    aria-label="Like">
    <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
          d="m11.645 20.91-.007-.003-.022-.012a15.247 15.247 0 0 1-.383-.218 25.18 25.18 0 0 1-4.244-3.17C4.688 15.36 2.25 12.174 2.25 8.25 2.25 5.322 4.714 3 7.688 3A5.5 5.5 0 0 1 12 5.052 5.5 5.5 0 0 1 16.313 3c2.973 0 5.437 2.322 5.437 5.25 0 3.925-2.438 7.111-4.739 9.256a25.175 25.175 0 0 1-4.244 3.17 15.247 15.247 0 0 1-.383.219l-.022.012-.007.004-.003.001a.752.752 0 0 1-.704 0l-.003-.001Z"></path>
      </svg>
    </span>
  </button>
</div>
---
import { Button } from '@/components/ui/button';

---

<Button icon="UserIcon">Left Icon</Button>
<Button rightIcon="ArrowRightIcon">Right Icon</Button>
<Button icon="HeartIcon" iconOnly aria-label="Like" />
---
// Imports
import { type HTMLAttributes } from "astro/types";
import { tv, type VariantProps } from "@utils/custom-tv";
import Icon from "./Icon.astro";
import {
  getDefaultClasses,
  getOutlinedClasses,
  getSoftClasses,
  type ColorName,
  colorPalette,
  getHardTextClass,
  getSoftTextClass,
  getGhostClasses,
  getButtonClasses,
} from "@utils/colorUtils";
import { twMerge } from "tailwind-merge";

// PropTypes for documentation
export const propTypes = {
  variant: {
    type: ["solid", "outline", "ghost", "soft", "text"],
    description: "The variant of the button",
    default: "solid",
  },
  size: {
    type: ["xs", "sm", "md", "lg", "xl", "2xl"],
    description: "The size of the button",
    default: "md",
  },
  color: {
    type: Object.keys(colorPalette),
    description: "The color scheme for the button",
    default: "blue",
  },
  shape: {
    type: ["square", "rounded", "pill"],
    description: "The shape of the button",
    default: "rounded",
  },
  fullWidth: {
    type: "boolean",
    description: "Whether the button should take full width",
    default: false,
  },
  icon: {
    type: "string | { name: string; class?: string }",
    description: "The icon to display in the button",
  },
  rightIcon: {
    type: "string | { name: string; class?: string }",
    description: "The icon to display on the right side of the button",
  },
  iconOnly: {
    type: "boolean",
    description: "Whether the button should only display an icon",
    default: false,
  },
  disabled: {
    type: "boolean",
    description: "Whether the button is disabled",
    default: false,
  },
  class: {
    type: "string",
    description: "Additional CSS classes to apply to the button",
  },
  as: {
    type: ["button", "a"],
    description: "The HTML element to render the button as",
    default: "button",
  },
  href: {
    type: "string",
    description: "The URL to link to when using an anchor tag",
  },
};

// Types and Interfaces
type ButtonVariants = VariantProps<typeof buttonStyles>;

interface Props extends HTMLAttributes<"button" | "a">, ButtonVariants {
  variant?: "solid" | "outline" | "ghost" | "soft" | "text";
  size?: "xs" | "sm" | "md" | "lg" | "xl" | "2xl";
  color?: ColorName;
  shape?: "square" | "rounded" | "pill";
  fullWidth?: boolean;
  icon?: string | { name: string; class?: string };
  rightIcon?: string | { name: string; class?: string };
  iconOnly?: boolean;
  disabled?: boolean;
  class?: string;
  as?: "button" | "a";
  href?: string;
}

// Component Logic
const {
  variant = "solid",
  size = "md",
  color = "blue",
  shape = "rounded",
  fullWidth = false,
  icon,
  rightIcon,
  iconOnly = false,
  disabled = false,
  class: className = "",
  as = "button",
  href,
  ...rest
} = Astro.props as Props;

const normalizeIcon = (icon: Props["icon"]) =>
  typeof icon === "string" ? { name: icon, class: "" } : icon;

const finalIcon = icon ? normalizeIcon(icon) : null;
const finalRightIcon = rightIcon ? normalizeIcon(rightIcon) : null;

// Styles
const buttonStyles = tv({
  base: "inline-flex items-center justify-center font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2",
  variants: {
    variant: {
      solid: getButtonClasses(color),
      outline: `${getOutlinedClasses(color)} border`,
      ghost: `${getGhostClasses(color)}`,
      soft: getSoftClasses(color),
      text: getSoftTextClass(color) + " hover:opacity-70",
    },
    size: {
      xs: "px-2 py-1 text-xs",
      sm: "px-2 py-1 text-sm",
      md: "px-3 py-2 text-sm",
      lg: "px-4 py-2 text-base",
      xl: "px-5 py-3 text-lg",
      "2xl": "px-6 py-4 text-xl",
    },
    shape: {
      square: "",
      rounded: "rounded-md",
      pill: "rounded-full",
    },
    fullWidth: {
      true: "w-full",
      false: "",
    },
    iconOnly: {
      true: "p-2",
      false: "",
    },
    disabled: {
      true: "opacity-50 cursor-not-allowed",
      false: "",
    },
  },

  defaultVariants: {
    variant: "solid",
    size: "md",
    shape: "rounded",
    fullWidth: false,
    iconOnly: false,
    disabled: false,
  },
});

const iconSize =
  size === "xs" || size === "sm"
    ? "w-4 h-4"
    : size === "2xl"
      ? "w-6 h-6"
      : "w-5 h-5";
const Element = as;
---

<Element
  class={twMerge(
    buttonStyles({
      variant,
      size,
      color,
      shape,
      fullWidth,
      iconOnly,
      disabled,
    }),
    className,
  )}
  disabled={as === "button" ? disabled : undefined}
  aria-disabled={disabled}
  href={as === "a" ? href : undefined}
  {...rest}
>
  {
    finalIcon && (
      <Icon
        name={finalIcon.name}
        class={twMerge(iconSize, !iconOnly && "mr-2", finalIcon.class)}
        aria-hidden="true"
      />
    )
  }
  {!iconOnly && <slot />}
  {
    finalRightIcon && (
      <Icon
        name={finalRightIcon.name}
        class={twMerge(iconSize, "ml-2", finalRightIcon.class)}
        aria-hidden="true"
      />
    )
  }
</Element>

Disabled Button

A button in a disabled state.

 
<button
  class="inline-flex items-center justify-center font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 bg-blue-500 hover:bg-blue-600 dark:bg-blue-600 dark:hover:bg-blue-500 text-white dark:text-white hover:text-blue-100 dark:hover:text-blue-200 px-3 py-2 text-sm rounded-md opacity-50 cursor-not-allowed"
  disabled
  aria-disabled="true">
  Disabled Button
</button>
---
import { Button } from '@/components/ui/button';

---

<Button disabled>Disabled Button</Button>
---
// Imports
import { type HTMLAttributes } from "astro/types";
import { tv, type VariantProps } from "@utils/custom-tv";
import Icon from "./Icon.astro";
import {
  getDefaultClasses,
  getOutlinedClasses,
  getSoftClasses,
  type ColorName,
  colorPalette,
  getHardTextClass,
  getSoftTextClass,
  getGhostClasses,
  getButtonClasses,
} from "@utils/colorUtils";
import { twMerge } from "tailwind-merge";

// PropTypes for documentation
export const propTypes = {
  variant: {
    type: ["solid", "outline", "ghost", "soft", "text"],
    description: "The variant of the button",
    default: "solid",
  },
  size: {
    type: ["xs", "sm", "md", "lg", "xl", "2xl"],
    description: "The size of the button",
    default: "md",
  },
  color: {
    type: Object.keys(colorPalette),
    description: "The color scheme for the button",
    default: "blue",
  },
  shape: {
    type: ["square", "rounded", "pill"],
    description: "The shape of the button",
    default: "rounded",
  },
  fullWidth: {
    type: "boolean",
    description: "Whether the button should take full width",
    default: false,
  },
  icon: {
    type: "string | { name: string; class?: string }",
    description: "The icon to display in the button",
  },
  rightIcon: {
    type: "string | { name: string; class?: string }",
    description: "The icon to display on the right side of the button",
  },
  iconOnly: {
    type: "boolean",
    description: "Whether the button should only display an icon",
    default: false,
  },
  disabled: {
    type: "boolean",
    description: "Whether the button is disabled",
    default: false,
  },
  class: {
    type: "string",
    description: "Additional CSS classes to apply to the button",
  },
  as: {
    type: ["button", "a"],
    description: "The HTML element to render the button as",
    default: "button",
  },
  href: {
    type: "string",
    description: "The URL to link to when using an anchor tag",
  },
};

// Types and Interfaces
type ButtonVariants = VariantProps<typeof buttonStyles>;

interface Props extends HTMLAttributes<"button" | "a">, ButtonVariants {
  variant?: "solid" | "outline" | "ghost" | "soft" | "text";
  size?: "xs" | "sm" | "md" | "lg" | "xl" | "2xl";
  color?: ColorName;
  shape?: "square" | "rounded" | "pill";
  fullWidth?: boolean;
  icon?: string | { name: string; class?: string };
  rightIcon?: string | { name: string; class?: string };
  iconOnly?: boolean;
  disabled?: boolean;
  class?: string;
  as?: "button" | "a";
  href?: string;
}

// Component Logic
const {
  variant = "solid",
  size = "md",
  color = "blue",
  shape = "rounded",
  fullWidth = false,
  icon,
  rightIcon,
  iconOnly = false,
  disabled = false,
  class: className = "",
  as = "button",
  href,
  ...rest
} = Astro.props as Props;

const normalizeIcon = (icon: Props["icon"]) =>
  typeof icon === "string" ? { name: icon, class: "" } : icon;

const finalIcon = icon ? normalizeIcon(icon) : null;
const finalRightIcon = rightIcon ? normalizeIcon(rightIcon) : null;

// Styles
const buttonStyles = tv({
  base: "inline-flex items-center justify-center font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2",
  variants: {
    variant: {
      solid: getButtonClasses(color),
      outline: `${getOutlinedClasses(color)} border`,
      ghost: `${getGhostClasses(color)}`,
      soft: getSoftClasses(color),
      text: getSoftTextClass(color) + " hover:opacity-70",
    },
    size: {
      xs: "px-2 py-1 text-xs",
      sm: "px-2 py-1 text-sm",
      md: "px-3 py-2 text-sm",
      lg: "px-4 py-2 text-base",
      xl: "px-5 py-3 text-lg",
      "2xl": "px-6 py-4 text-xl",
    },
    shape: {
      square: "",
      rounded: "rounded-md",
      pill: "rounded-full",
    },
    fullWidth: {
      true: "w-full",
      false: "",
    },
    iconOnly: {
      true: "p-2",
      false: "",
    },
    disabled: {
      true: "opacity-50 cursor-not-allowed",
      false: "",
    },
  },

  defaultVariants: {
    variant: "solid",
    size: "md",
    shape: "rounded",
    fullWidth: false,
    iconOnly: false,
    disabled: false,
  },
});

const iconSize =
  size === "xs" || size === "sm"
    ? "w-4 h-4"
    : size === "2xl"
      ? "w-6 h-6"
      : "w-5 h-5";
const Element = as;
---

<Element
  class={twMerge(
    buttonStyles({
      variant,
      size,
      color,
      shape,
      fullWidth,
      iconOnly,
      disabled,
    }),
    className,
  )}
  disabled={as === "button" ? disabled : undefined}
  aria-disabled={disabled}
  href={as === "a" ? href : undefined}
  {...rest}
>
  {
    finalIcon && (
      <Icon
        name={finalIcon.name}
        class={twMerge(iconSize, !iconOnly && "mr-2", finalIcon.class)}
        aria-hidden="true"
      />
    )
  }
  {!iconOnly && <slot />}
  {
    finalRightIcon && (
      <Icon
        name={finalRightIcon.name}
        class={twMerge(iconSize, "ml-2", finalRightIcon.class)}
        aria-hidden="true"
      />
    )
  }
</Element>

Button Variants with Colors

Different button variants with various color options.

 
<div class="space-x-2">
  <button
    class="inline-flex items-center justify-center font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 bg-blue-500 hover:bg-blue-600 dark:bg-blue-600 dark:hover:bg-blue-500 text-white dark:text-white hover:text-blue-100 dark:hover:text-blue-200 px-3 py-2 text-sm rounded-md">
    Solid Blue
  </button>
  <button
    class="inline-flex items-center justify-center font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 bg-transparent text-green-500 dark:text-green-400 hover:text-green-700 dark:hover:text-green-200 border-green-500 dark:border-green-400 hover:border-green-700 dark:hover:border-green-200 border px-3 py-2 text-sm rounded-md">
    Outline Green
  </button>
  <button
    class="inline-flex items-center justify-center font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 bg-transparent text-red-500 dark:text-red-400 hover:text-red-700 dark:hover:text-red-200 hover:bg-red-200 dark:hover:bg-red-700 px-3 py-2 text-sm rounded-md">
    Ghost Red
  </button>
  <button
    class="inline-flex items-center justify-center font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 bg-yellow-100 dark:bg-yellow-800 hover:bg-yellow-200 dark:hover:bg-yellow-700 text-yellow-700 dark:text-yellow-100 border-yellow-200 dark:border-yellow-600 px-3 py-2 text-sm rounded-md">
    Soft Yellow
  </button>
  <button
    class="inline-flex items-center justify-center font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 text-purple-700 dark:text-purple-100 hover:opacity-70 px-3 py-2 text-sm rounded-md">
    Text Purple
  </button>
</div>
---
import { Button } from '@/components/ui/button';

---

<Button variant="solid" color="blue">Solid Blue</Button>
<Button variant="outline" color="green">Outline Green</Button>
<Button variant="ghost" color="red">Ghost Red</Button>
<Button variant="soft" color="yellow">Soft Yellow</Button>
<Button variant="text" color="purple">Text Purple</Button>
---
// Imports
import { type HTMLAttributes } from "astro/types";
import { tv, type VariantProps } from "@utils/custom-tv";
import Icon from "./Icon.astro";
import {
  getDefaultClasses,
  getOutlinedClasses,
  getSoftClasses,
  type ColorName,
  colorPalette,
  getHardTextClass,
  getSoftTextClass,
  getGhostClasses,
  getButtonClasses,
} from "@utils/colorUtils";
import { twMerge } from "tailwind-merge";

// PropTypes for documentation
export const propTypes = {
  variant: {
    type: ["solid", "outline", "ghost", "soft", "text"],
    description: "The variant of the button",
    default: "solid",
  },
  size: {
    type: ["xs", "sm", "md", "lg", "xl", "2xl"],
    description: "The size of the button",
    default: "md",
  },
  color: {
    type: Object.keys(colorPalette),
    description: "The color scheme for the button",
    default: "blue",
  },
  shape: {
    type: ["square", "rounded", "pill"],
    description: "The shape of the button",
    default: "rounded",
  },
  fullWidth: {
    type: "boolean",
    description: "Whether the button should take full width",
    default: false,
  },
  icon: {
    type: "string | { name: string; class?: string }",
    description: "The icon to display in the button",
  },
  rightIcon: {
    type: "string | { name: string; class?: string }",
    description: "The icon to display on the right side of the button",
  },
  iconOnly: {
    type: "boolean",
    description: "Whether the button should only display an icon",
    default: false,
  },
  disabled: {
    type: "boolean",
    description: "Whether the button is disabled",
    default: false,
  },
  class: {
    type: "string",
    description: "Additional CSS classes to apply to the button",
  },
  as: {
    type: ["button", "a"],
    description: "The HTML element to render the button as",
    default: "button",
  },
  href: {
    type: "string",
    description: "The URL to link to when using an anchor tag",
  },
};

// Types and Interfaces
type ButtonVariants = VariantProps<typeof buttonStyles>;

interface Props extends HTMLAttributes<"button" | "a">, ButtonVariants {
  variant?: "solid" | "outline" | "ghost" | "soft" | "text";
  size?: "xs" | "sm" | "md" | "lg" | "xl" | "2xl";
  color?: ColorName;
  shape?: "square" | "rounded" | "pill";
  fullWidth?: boolean;
  icon?: string | { name: string; class?: string };
  rightIcon?: string | { name: string; class?: string };
  iconOnly?: boolean;
  disabled?: boolean;
  class?: string;
  as?: "button" | "a";
  href?: string;
}

// Component Logic
const {
  variant = "solid",
  size = "md",
  color = "blue",
  shape = "rounded",
  fullWidth = false,
  icon,
  rightIcon,
  iconOnly = false,
  disabled = false,
  class: className = "",
  as = "button",
  href,
  ...rest
} = Astro.props as Props;

const normalizeIcon = (icon: Props["icon"]) =>
  typeof icon === "string" ? { name: icon, class: "" } : icon;

const finalIcon = icon ? normalizeIcon(icon) : null;
const finalRightIcon = rightIcon ? normalizeIcon(rightIcon) : null;

// Styles
const buttonStyles = tv({
  base: "inline-flex items-center justify-center font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2",
  variants: {
    variant: {
      solid: getButtonClasses(color),
      outline: `${getOutlinedClasses(color)} border`,
      ghost: `${getGhostClasses(color)}`,
      soft: getSoftClasses(color),
      text: getSoftTextClass(color) + " hover:opacity-70",
    },
    size: {
      xs: "px-2 py-1 text-xs",
      sm: "px-2 py-1 text-sm",
      md: "px-3 py-2 text-sm",
      lg: "px-4 py-2 text-base",
      xl: "px-5 py-3 text-lg",
      "2xl": "px-6 py-4 text-xl",
    },
    shape: {
      square: "",
      rounded: "rounded-md",
      pill: "rounded-full",
    },
    fullWidth: {
      true: "w-full",
      false: "",
    },
    iconOnly: {
      true: "p-2",
      false: "",
    },
    disabled: {
      true: "opacity-50 cursor-not-allowed",
      false: "",
    },
  },

  defaultVariants: {
    variant: "solid",
    size: "md",
    shape: "rounded",
    fullWidth: false,
    iconOnly: false,
    disabled: false,
  },
});

const iconSize =
  size === "xs" || size === "sm"
    ? "w-4 h-4"
    : size === "2xl"
      ? "w-6 h-6"
      : "w-5 h-5";
const Element = as;
---

<Element
  class={twMerge(
    buttonStyles({
      variant,
      size,
      color,
      shape,
      fullWidth,
      iconOnly,
      disabled,
    }),
    className,
  )}
  disabled={as === "button" ? disabled : undefined}
  aria-disabled={disabled}
  href={as === "a" ? href : undefined}
  {...rest}
>
  {
    finalIcon && (
      <Icon
        name={finalIcon.name}
        class={twMerge(iconSize, !iconOnly && "mr-2", finalIcon.class)}
        aria-hidden="true"
      />
    )
  }
  {!iconOnly && <slot />}
  {
    finalRightIcon && (
      <Icon
        name={finalRightIcon.name}
        class={twMerge(iconSize, "ml-2", finalRightIcon.class)}
        aria-hidden="true"
      />
    )
  }
</Element>

Button Sizes with Icons

Buttons of different sizes with icons.

 
<div class="block space-x-6">
  <button
    class="inline-flex items-center justify-center font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 bg-blue-500 hover:bg-blue-600 dark:bg-blue-600 dark:hover:bg-blue-500 text-white dark:text-white hover:text-blue-100 dark:hover:text-blue-200 px-2 py-1 text-xs rounded-md">
    <span class="inline-block size-5 w-4 h-4 mr-2">
      <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="M10.788 3.21c.448-1.077 1.976-1.077 2.424 0l2.082 5.006 5.404.434c1.164.093 1.636 1.545.749 2.305l-4.117 3.527 1.257 5.273c.271 1.136-.964 2.033-1.96 1.425L12 18.354 7.373 21.18c-.996.608-2.231-.29-1.96-1.425l1.257-5.273-4.117-3.527c-.887-.76-.415-2.212.749-2.305l5.404-.434 2.082-5.005Z"
          clip-rule="evenodd"></path>
      </svg>
    </span>
    Extra Small
  </button>
  <button
    class="inline-flex items-center justify-center font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 bg-blue-500 hover:bg-blue-600 dark:bg-blue-600 dark:hover:bg-blue-500 text-white dark:text-white hover:text-blue-100 dark:hover:text-blue-200 px-2 py-1 text-sm rounded-md">
    <span class="inline-block size-5 w-4 h-4 mr-2">
      <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="M10.788 3.21c.448-1.077 1.976-1.077 2.424 0l2.082 5.006 5.404.434c1.164.093 1.636 1.545.749 2.305l-4.117 3.527 1.257 5.273c.271 1.136-.964 2.033-1.96 1.425L12 18.354 7.373 21.18c-.996.608-2.231-.29-1.96-1.425l1.257-5.273-4.117-3.527c-.887-.76-.415-2.212.749-2.305l5.404-.434 2.082-5.005Z"
          clip-rule="evenodd"></path>
      </svg>
    </span>
    Small
  </button>
  <button
    class="inline-flex items-center justify-center font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 bg-blue-500 hover:bg-blue-600 dark:bg-blue-600 dark:hover:bg-blue-500 text-white dark:text-white hover:text-blue-100 dark:hover:text-blue-200 px-3 py-2 text-sm rounded-md">
    <span class="inline-block size-5 w-5 h-5 mr-2">
      <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="M10.788 3.21c.448-1.077 1.976-1.077 2.424 0l2.082 5.006 5.404.434c1.164.093 1.636 1.545.749 2.305l-4.117 3.527 1.257 5.273c.271 1.136-.964 2.033-1.96 1.425L12 18.354 7.373 21.18c-.996.608-2.231-.29-1.96-1.425l1.257-5.273-4.117-3.527c-.887-.76-.415-2.212.749-2.305l5.404-.434 2.082-5.005Z"
          clip-rule="evenodd"></path>
      </svg>
    </span>
    Medium
  </button>
  <button
    class="inline-flex items-center justify-center font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 bg-blue-500 hover:bg-blue-600 dark:bg-blue-600 dark:hover:bg-blue-500 text-white dark:text-white hover:text-blue-100 dark:hover:text-blue-200 px-4 py-2 text-base rounded-md">
    <span class="inline-block size-5 w-5 h-5 mr-2">
      <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="M10.788 3.21c.448-1.077 1.976-1.077 2.424 0l2.082 5.006 5.404.434c1.164.093 1.636 1.545.749 2.305l-4.117 3.527 1.257 5.273c.271 1.136-.964 2.033-1.96 1.425L12 18.354 7.373 21.18c-.996.608-2.231-.29-1.96-1.425l1.257-5.273-4.117-3.527c-.887-.76-.415-2.212.749-2.305l5.404-.434 2.082-5.005Z"
          clip-rule="evenodd"></path>
      </svg>
    </span>
    Large
  </button>
  <button
    class="inline-flex items-center justify-center font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 bg-blue-500 hover:bg-blue-600 dark:bg-blue-600 dark:hover:bg-blue-500 text-white dark:text-white hover:text-blue-100 dark:hover:text-blue-200 px-5 py-3 text-lg rounded-md">
    <span class="inline-block size-5 w-5 h-5 mr-2">
      <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="M10.788 3.21c.448-1.077 1.976-1.077 2.424 0l2.082 5.006 5.404.434c1.164.093 1.636 1.545.749 2.305l-4.117 3.527 1.257 5.273c.271 1.136-.964 2.033-1.96 1.425L12 18.354 7.373 21.18c-.996.608-2.231-.29-1.96-1.425l1.257-5.273-4.117-3.527c-.887-.76-.415-2.212.749-2.305l5.404-.434 2.082-5.005Z"
          clip-rule="evenodd"></path>
      </svg>
    </span>
    Extra Large
  </button>
  <button
    class="inline-flex items-center justify-center font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 bg-blue-500 hover:bg-blue-600 dark:bg-blue-600 dark:hover:bg-blue-500 text-white dark:text-white hover:text-blue-100 dark:hover:text-blue-200 px-6 py-4 text-xl rounded-md">
    <span class="inline-block size-5 w-6 h-6 mr-2">
      <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="M10.788 3.21c.448-1.077 1.976-1.077 2.424 0l2.082 5.006 5.404.434c1.164.093 1.636 1.545.749 2.305l-4.117 3.527 1.257 5.273c.271 1.136-.964 2.033-1.96 1.425L12 18.354 7.373 21.18c-.996.608-2.231-.29-1.96-1.425l1.257-5.273-4.117-3.527c-.887-.76-.415-2.212.749-2.305l5.404-.434 2.082-5.005Z"
          clip-rule="evenodd"></path>
      </svg>
    </span>
    2X Large
  </button>
</div>
---
import { Button } from '@/components/ui/button';

---

<Button size="xs" icon="StarIcon">Extra Small</Button>
<Button size="sm" icon="StarIcon">Small</Button>
<Button size="md" icon="StarIcon">Medium</Button>
<Button size="lg" icon="StarIcon">Large</Button>
<Button size="xl" icon="StarIcon">Extra Large</Button>
<Button size="2xl" icon="StarIcon">2X Large</Button>
---
// Imports
import { type HTMLAttributes } from "astro/types";
import { tv, type VariantProps } from "@utils/custom-tv";
import Icon from "./Icon.astro";
import {
  getDefaultClasses,
  getOutlinedClasses,
  getSoftClasses,
  type ColorName,
  colorPalette,
  getHardTextClass,
  getSoftTextClass,
  getGhostClasses,
  getButtonClasses,
} from "@utils/colorUtils";
import { twMerge } from "tailwind-merge";

// PropTypes for documentation
export const propTypes = {
  variant: {
    type: ["solid", "outline", "ghost", "soft", "text"],
    description: "The variant of the button",
    default: "solid",
  },
  size: {
    type: ["xs", "sm", "md", "lg", "xl", "2xl"],
    description: "The size of the button",
    default: "md",
  },
  color: {
    type: Object.keys(colorPalette),
    description: "The color scheme for the button",
    default: "blue",
  },
  shape: {
    type: ["square", "rounded", "pill"],
    description: "The shape of the button",
    default: "rounded",
  },
  fullWidth: {
    type: "boolean",
    description: "Whether the button should take full width",
    default: false,
  },
  icon: {
    type: "string | { name: string; class?: string }",
    description: "The icon to display in the button",
  },
  rightIcon: {
    type: "string | { name: string; class?: string }",
    description: "The icon to display on the right side of the button",
  },
  iconOnly: {
    type: "boolean",
    description: "Whether the button should only display an icon",
    default: false,
  },
  disabled: {
    type: "boolean",
    description: "Whether the button is disabled",
    default: false,
  },
  class: {
    type: "string",
    description: "Additional CSS classes to apply to the button",
  },
  as: {
    type: ["button", "a"],
    description: "The HTML element to render the button as",
    default: "button",
  },
  href: {
    type: "string",
    description: "The URL to link to when using an anchor tag",
  },
};

// Types and Interfaces
type ButtonVariants = VariantProps<typeof buttonStyles>;

interface Props extends HTMLAttributes<"button" | "a">, ButtonVariants {
  variant?: "solid" | "outline" | "ghost" | "soft" | "text";
  size?: "xs" | "sm" | "md" | "lg" | "xl" | "2xl";
  color?: ColorName;
  shape?: "square" | "rounded" | "pill";
  fullWidth?: boolean;
  icon?: string | { name: string; class?: string };
  rightIcon?: string | { name: string; class?: string };
  iconOnly?: boolean;
  disabled?: boolean;
  class?: string;
  as?: "button" | "a";
  href?: string;
}

// Component Logic
const {
  variant = "solid",
  size = "md",
  color = "blue",
  shape = "rounded",
  fullWidth = false,
  icon,
  rightIcon,
  iconOnly = false,
  disabled = false,
  class: className = "",
  as = "button",
  href,
  ...rest
} = Astro.props as Props;

const normalizeIcon = (icon: Props["icon"]) =>
  typeof icon === "string" ? { name: icon, class: "" } : icon;

const finalIcon = icon ? normalizeIcon(icon) : null;
const finalRightIcon = rightIcon ? normalizeIcon(rightIcon) : null;

// Styles
const buttonStyles = tv({
  base: "inline-flex items-center justify-center font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2",
  variants: {
    variant: {
      solid: getButtonClasses(color),
      outline: `${getOutlinedClasses(color)} border`,
      ghost: `${getGhostClasses(color)}`,
      soft: getSoftClasses(color),
      text: getSoftTextClass(color) + " hover:opacity-70",
    },
    size: {
      xs: "px-2 py-1 text-xs",
      sm: "px-2 py-1 text-sm",
      md: "px-3 py-2 text-sm",
      lg: "px-4 py-2 text-base",
      xl: "px-5 py-3 text-lg",
      "2xl": "px-6 py-4 text-xl",
    },
    shape: {
      square: "",
      rounded: "rounded-md",
      pill: "rounded-full",
    },
    fullWidth: {
      true: "w-full",
      false: "",
    },
    iconOnly: {
      true: "p-2",
      false: "",
    },
    disabled: {
      true: "opacity-50 cursor-not-allowed",
      false: "",
    },
  },

  defaultVariants: {
    variant: "solid",
    size: "md",
    shape: "rounded",
    fullWidth: false,
    iconOnly: false,
    disabled: false,
  },
});

const iconSize =
  size === "xs" || size === "sm"
    ? "w-4 h-4"
    : size === "2xl"
      ? "w-6 h-6"
      : "w-5 h-5";
const Element = as;
---

<Element
  class={twMerge(
    buttonStyles({
      variant,
      size,
      color,
      shape,
      fullWidth,
      iconOnly,
      disabled,
    }),
    className,
  )}
  disabled={as === "button" ? disabled : undefined}
  aria-disabled={disabled}
  href={as === "a" ? href : undefined}
  {...rest}
>
  {
    finalIcon && (
      <Icon
        name={finalIcon.name}
        class={twMerge(iconSize, !iconOnly && "mr-2", finalIcon.class)}
        aria-hidden="true"
      />
    )
  }
  {!iconOnly && <slot />}
  {
    finalRightIcon && (
      <Icon
        name={finalRightIcon.name}
        class={twMerge(iconSize, "ml-2", finalRightIcon.class)}
        aria-hidden="true"
      />
    )
  }
</Element>

Icon-Only Buttons with Different Shapes

Icon-only buttons with square, rounded, and pill shapes.

 
<div class="space-x-2">
  <button
    class="inline-flex items-center justify-center font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 bg-blue-500 hover:bg-blue-600 dark:bg-blue-600 dark:hover:bg-blue-500 text-white dark:text-white hover:text-blue-100 dark:hover:text-blue-200 text-sm p-2"
    aria-label="Like">
    <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
          d="m11.645 20.91-.007-.003-.022-.012a15.247 15.247 0 0 1-.383-.218 25.18 25.18 0 0 1-4.244-3.17C4.688 15.36 2.25 12.174 2.25 8.25 2.25 5.322 4.714 3 7.688 3A5.5 5.5 0 0 1 12 5.052 5.5 5.5 0 0 1 16.313 3c2.973 0 5.437 2.322 5.437 5.25 0 3.925-2.438 7.111-4.739 9.256a25.175 25.175 0 0 1-4.244 3.17 15.247 15.247 0 0 1-.383.219l-.022.012-.007.004-.003.001a.752.752 0 0 1-.704 0l-.003-.001Z"></path>
      </svg>
    </span>
  </button>
  <button
    class="inline-flex items-center justify-center font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 bg-blue-500 hover:bg-blue-600 dark:bg-blue-600 dark:hover:bg-blue-500 text-white dark:text-white hover:text-blue-100 dark:hover:text-blue-200 text-sm rounded-md p-2"
    aria-label="Like">
    <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
          d="m11.645 20.91-.007-.003-.022-.012a15.247 15.247 0 0 1-.383-.218 25.18 25.18 0 0 1-4.244-3.17C4.688 15.36 2.25 12.174 2.25 8.25 2.25 5.322 4.714 3 7.688 3A5.5 5.5 0 0 1 12 5.052 5.5 5.5 0 0 1 16.313 3c2.973 0 5.437 2.322 5.437 5.25 0 3.925-2.438 7.111-4.739 9.256a25.175 25.175 0 0 1-4.244 3.17 15.247 15.247 0 0 1-.383.219l-.022.012-.007.004-.003.001a.752.752 0 0 1-.704 0l-.003-.001Z"></path>
      </svg>
    </span>
  </button>
  <button
    class="inline-flex items-center justify-center font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 bg-blue-500 hover:bg-blue-600 dark:bg-blue-600 dark:hover:bg-blue-500 text-white dark:text-white hover:text-blue-100 dark:hover:text-blue-200 text-sm rounded-full p-2"
    aria-label="Like">
    <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
          d="m11.645 20.91-.007-.003-.022-.012a15.247 15.247 0 0 1-.383-.218 25.18 25.18 0 0 1-4.244-3.17C4.688 15.36 2.25 12.174 2.25 8.25 2.25 5.322 4.714 3 7.688 3A5.5 5.5 0 0 1 12 5.052 5.5 5.5 0 0 1 16.313 3c2.973 0 5.437 2.322 5.437 5.25 0 3.925-2.438 7.111-4.739 9.256a25.175 25.175 0 0 1-4.244 3.17 15.247 15.247 0 0 1-.383.219l-.022.012-.007.004-.003.001a.752.752 0 0 1-.704 0l-.003-.001Z"></path>
      </svg>
    </span>
  </button>
</div>
---
import { Button } from '@/components/ui/button';

---

<Button icon="HeartIcon" iconOnly shape="square" aria-label="Like" />
<Button icon="HeartIcon" iconOnly shape="rounded" aria-label="Like" />
<Button icon="HeartIcon" iconOnly shape="pill" aria-label="Like" />
---
// Imports
import { type HTMLAttributes } from "astro/types";
import { tv, type VariantProps } from "@utils/custom-tv";
import Icon from "./Icon.astro";
import {
  getDefaultClasses,
  getOutlinedClasses,
  getSoftClasses,
  type ColorName,
  colorPalette,
  getHardTextClass,
  getSoftTextClass,
  getGhostClasses,
  getButtonClasses,
} from "@utils/colorUtils";
import { twMerge } from "tailwind-merge";

// PropTypes for documentation
export const propTypes = {
  variant: {
    type: ["solid", "outline", "ghost", "soft", "text"],
    description: "The variant of the button",
    default: "solid",
  },
  size: {
    type: ["xs", "sm", "md", "lg", "xl", "2xl"],
    description: "The size of the button",
    default: "md",
  },
  color: {
    type: Object.keys(colorPalette),
    description: "The color scheme for the button",
    default: "blue",
  },
  shape: {
    type: ["square", "rounded", "pill"],
    description: "The shape of the button",
    default: "rounded",
  },
  fullWidth: {
    type: "boolean",
    description: "Whether the button should take full width",
    default: false,
  },
  icon: {
    type: "string | { name: string; class?: string }",
    description: "The icon to display in the button",
  },
  rightIcon: {
    type: "string | { name: string; class?: string }",
    description: "The icon to display on the right side of the button",
  },
  iconOnly: {
    type: "boolean",
    description: "Whether the button should only display an icon",
    default: false,
  },
  disabled: {
    type: "boolean",
    description: "Whether the button is disabled",
    default: false,
  },
  class: {
    type: "string",
    description: "Additional CSS classes to apply to the button",
  },
  as: {
    type: ["button", "a"],
    description: "The HTML element to render the button as",
    default: "button",
  },
  href: {
    type: "string",
    description: "The URL to link to when using an anchor tag",
  },
};

// Types and Interfaces
type ButtonVariants = VariantProps<typeof buttonStyles>;

interface Props extends HTMLAttributes<"button" | "a">, ButtonVariants {
  variant?: "solid" | "outline" | "ghost" | "soft" | "text";
  size?: "xs" | "sm" | "md" | "lg" | "xl" | "2xl";
  color?: ColorName;
  shape?: "square" | "rounded" | "pill";
  fullWidth?: boolean;
  icon?: string | { name: string; class?: string };
  rightIcon?: string | { name: string; class?: string };
  iconOnly?: boolean;
  disabled?: boolean;
  class?: string;
  as?: "button" | "a";
  href?: string;
}

// Component Logic
const {
  variant = "solid",
  size = "md",
  color = "blue",
  shape = "rounded",
  fullWidth = false,
  icon,
  rightIcon,
  iconOnly = false,
  disabled = false,
  class: className = "",
  as = "button",
  href,
  ...rest
} = Astro.props as Props;

const normalizeIcon = (icon: Props["icon"]) =>
  typeof icon === "string" ? { name: icon, class: "" } : icon;

const finalIcon = icon ? normalizeIcon(icon) : null;
const finalRightIcon = rightIcon ? normalizeIcon(rightIcon) : null;

// Styles
const buttonStyles = tv({
  base: "inline-flex items-center justify-center font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2",
  variants: {
    variant: {
      solid: getButtonClasses(color),
      outline: `${getOutlinedClasses(color)} border`,
      ghost: `${getGhostClasses(color)}`,
      soft: getSoftClasses(color),
      text: getSoftTextClass(color) + " hover:opacity-70",
    },
    size: {
      xs: "px-2 py-1 text-xs",
      sm: "px-2 py-1 text-sm",
      md: "px-3 py-2 text-sm",
      lg: "px-4 py-2 text-base",
      xl: "px-5 py-3 text-lg",
      "2xl": "px-6 py-4 text-xl",
    },
    shape: {
      square: "",
      rounded: "rounded-md",
      pill: "rounded-full",
    },
    fullWidth: {
      true: "w-full",
      false: "",
    },
    iconOnly: {
      true: "p-2",
      false: "",
    },
    disabled: {
      true: "opacity-50 cursor-not-allowed",
      false: "",
    },
  },

  defaultVariants: {
    variant: "solid",
    size: "md",
    shape: "rounded",
    fullWidth: false,
    iconOnly: false,
    disabled: false,
  },
});

const iconSize =
  size === "xs" || size === "sm"
    ? "w-4 h-4"
    : size === "2xl"
      ? "w-6 h-6"
      : "w-5 h-5";
const Element = as;
---

<Element
  class={twMerge(
    buttonStyles({
      variant,
      size,
      color,
      shape,
      fullWidth,
      iconOnly,
      disabled,
    }),
    className,
  )}
  disabled={as === "button" ? disabled : undefined}
  aria-disabled={disabled}
  href={as === "a" ? href : undefined}
  {...rest}
>
  {
    finalIcon && (
      <Icon
        name={finalIcon.name}
        class={twMerge(iconSize, !iconOnly && "mr-2", finalIcon.class)}
        aria-hidden="true"
      />
    )
  }
  {!iconOnly && <slot />}
  {
    finalRightIcon && (
      <Icon
        name={finalRightIcon.name}
        class={twMerge(iconSize, "ml-2", finalRightIcon.class)}
        aria-hidden="true"
      />
    )
  }
</Element>

Buttons with Two Icons

Buttons with both left and right icons.

 
<div class="space-x-2">
  <button
    class="inline-flex items-center justify-center font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 bg-blue-500 hover:bg-blue-600 dark:bg-blue-600 dark:hover:bg-blue-500 text-white dark:text-white hover:text-blue-100 dark:hover:text-blue-200 px-3 py-2 text-sm rounded-md">
    <span class="inline-block size-5 w-5 h-5 mr-2">
      <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 12Zm11.378-3.917c-.89-.777-2.366-.777-3.255 0a.75.75 0 0 1-.988-1.129c1.454-1.272 3.776-1.272 5.23 0 1.513 1.324 1.513 3.518 0 4.842a3.75 3.75 0 0 1-.837.552c-.676.328-1.028.774-1.028 1.152v.75a.75.75 0 0 1-1.5 0v-.75c0-1.279 1.06-2.107 1.875-2.502.182-.088.351-.199.503-.331.83-.727.83-1.857 0-2.584ZM12 18a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5Z"
          clip-rule="evenodd"></path>
      </svg>
      <span class="sr-only">Unknown icon</span>
    </span>
    Send Email
    <span class="inline-block size-5 w-5 h-5 ml-2">
      <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="M12.97 3.97a.75.75 0 0 1 1.06 0l7.5 7.5a.75.75 0 0 1 0 1.06l-7.5 7.5a.75.75 0 1 1-1.06-1.06l6.22-6.22H3a.75.75 0 0 1 0-1.5h16.19l-6.22-6.22a.75.75 0 0 1 0-1.06Z"
          clip-rule="evenodd"></path>
      </svg>
    </span>
  </button>
  <button
    class="inline-flex items-center justify-center font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 bg-blue-500 hover:bg-blue-600 dark:bg-blue-600 dark:hover:bg-blue-500 text-white dark:text-white hover:text-blue-100 dark:hover:text-blue-200 px-3 py-2 text-sm rounded-md">
    <span class="inline-block size-5 w-5 h-5 mr-2">
      <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="M4.5 9.75a6 6 0 0 1 11.573-2.226 3.75 3.75 0 0 1 4.133 4.303A4.5 4.5 0 0 1 18 20.25H6.75a5.25 5.25 0 0 1-2.23-10.004 6.072 6.072 0 0 1-.02-.496Z"
          clip-rule="evenodd"></path>
      </svg>
    </span>
    Download
    <span class="inline-block size-5 w-5 h-5 ml-2">
      <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="M12 2.25a.75.75 0 0 1 .75.75v16.19l6.22-6.22a.75.75 0 1 1 1.06 1.06l-7.5 7.5a.75.75 0 0 1-1.06 0l-7.5-7.5a.75.75 0 1 1 1.06-1.06l6.22 6.22V3a.75.75 0 0 1 .75-.75Z"
          clip-rule="evenodd"></path>
      </svg>
    </span>
  </button>
</div>
---
import { Button } from '@/components/ui/button';

---

<Button icon="MailIcon" rightIcon="ArrowRightIcon">Send Email</Button>
<Button icon="CloudIcon" rightIcon="ArrowDownIcon">Download</Button>
---
// Imports
import { type HTMLAttributes } from "astro/types";
import { tv, type VariantProps } from "@utils/custom-tv";
import Icon from "./Icon.astro";
import {
  getDefaultClasses,
  getOutlinedClasses,
  getSoftClasses,
  type ColorName,
  colorPalette,
  getHardTextClass,
  getSoftTextClass,
  getGhostClasses,
  getButtonClasses,
} from "@utils/colorUtils";
import { twMerge } from "tailwind-merge";

// PropTypes for documentation
export const propTypes = {
  variant: {
    type: ["solid", "outline", "ghost", "soft", "text"],
    description: "The variant of the button",
    default: "solid",
  },
  size: {
    type: ["xs", "sm", "md", "lg", "xl", "2xl"],
    description: "The size of the button",
    default: "md",
  },
  color: {
    type: Object.keys(colorPalette),
    description: "The color scheme for the button",
    default: "blue",
  },
  shape: {
    type: ["square", "rounded", "pill"],
    description: "The shape of the button",
    default: "rounded",
  },
  fullWidth: {
    type: "boolean",
    description: "Whether the button should take full width",
    default: false,
  },
  icon: {
    type: "string | { name: string; class?: string }",
    description: "The icon to display in the button",
  },
  rightIcon: {
    type: "string | { name: string; class?: string }",
    description: "The icon to display on the right side of the button",
  },
  iconOnly: {
    type: "boolean",
    description: "Whether the button should only display an icon",
    default: false,
  },
  disabled: {
    type: "boolean",
    description: "Whether the button is disabled",
    default: false,
  },
  class: {
    type: "string",
    description: "Additional CSS classes to apply to the button",
  },
  as: {
    type: ["button", "a"],
    description: "The HTML element to render the button as",
    default: "button",
  },
  href: {
    type: "string",
    description: "The URL to link to when using an anchor tag",
  },
};

// Types and Interfaces
type ButtonVariants = VariantProps<typeof buttonStyles>;

interface Props extends HTMLAttributes<"button" | "a">, ButtonVariants {
  variant?: "solid" | "outline" | "ghost" | "soft" | "text";
  size?: "xs" | "sm" | "md" | "lg" | "xl" | "2xl";
  color?: ColorName;
  shape?: "square" | "rounded" | "pill";
  fullWidth?: boolean;
  icon?: string | { name: string; class?: string };
  rightIcon?: string | { name: string; class?: string };
  iconOnly?: boolean;
  disabled?: boolean;
  class?: string;
  as?: "button" | "a";
  href?: string;
}

// Component Logic
const {
  variant = "solid",
  size = "md",
  color = "blue",
  shape = "rounded",
  fullWidth = false,
  icon,
  rightIcon,
  iconOnly = false,
  disabled = false,
  class: className = "",
  as = "button",
  href,
  ...rest
} = Astro.props as Props;

const normalizeIcon = (icon: Props["icon"]) =>
  typeof icon === "string" ? { name: icon, class: "" } : icon;

const finalIcon = icon ? normalizeIcon(icon) : null;
const finalRightIcon = rightIcon ? normalizeIcon(rightIcon) : null;

// Styles
const buttonStyles = tv({
  base: "inline-flex items-center justify-center font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2",
  variants: {
    variant: {
      solid: getButtonClasses(color),
      outline: `${getOutlinedClasses(color)} border`,
      ghost: `${getGhostClasses(color)}`,
      soft: getSoftClasses(color),
      text: getSoftTextClass(color) + " hover:opacity-70",
    },
    size: {
      xs: "px-2 py-1 text-xs",
      sm: "px-2 py-1 text-sm",
      md: "px-3 py-2 text-sm",
      lg: "px-4 py-2 text-base",
      xl: "px-5 py-3 text-lg",
      "2xl": "px-6 py-4 text-xl",
    },
    shape: {
      square: "",
      rounded: "rounded-md",
      pill: "rounded-full",
    },
    fullWidth: {
      true: "w-full",
      false: "",
    },
    iconOnly: {
      true: "p-2",
      false: "",
    },
    disabled: {
      true: "opacity-50 cursor-not-allowed",
      false: "",
    },
  },

  defaultVariants: {
    variant: "solid",
    size: "md",
    shape: "rounded",
    fullWidth: false,
    iconOnly: false,
    disabled: false,
  },
});

const iconSize =
  size === "xs" || size === "sm"
    ? "w-4 h-4"
    : size === "2xl"
      ? "w-6 h-6"
      : "w-5 h-5";
const Element = as;
---

<Element
  class={twMerge(
    buttonStyles({
      variant,
      size,
      color,
      shape,
      fullWidth,
      iconOnly,
      disabled,
    }),
    className,
  )}
  disabled={as === "button" ? disabled : undefined}
  aria-disabled={disabled}
  href={as === "a" ? href : undefined}
  {...rest}
>
  {
    finalIcon && (
      <Icon
        name={finalIcon.name}
        class={twMerge(iconSize, !iconOnly && "mr-2", finalIcon.class)}
        aria-hidden="true"
      />
    )
  }
  {!iconOnly && <slot />}
  {
    finalRightIcon && (
      <Icon
        name={finalRightIcon.name}
        class={twMerge(iconSize, "ml-2", finalRightIcon.class)}
        aria-hidden="true"
      />
    )
  }
</Element>

Custom Styled Button

A button with custom CSS classes applied.

 
<button
  class="inline-flex items-center justify-center focus:outline-none focus:ring-2 focus:ring-offset-2 bg-blue-500 hover:bg-blue-600 dark:bg-blue-600 dark:hover:bg-blue-500 dark:text-white hover:text-blue-100 dark:hover:text-blue-200 text-sm bg-gradient-to-r from-purple-500 to-pink-500 text-white font-bold py-2 px-4 rounded-full shadow-lg hover:shadow-xl transition-shadow duration-300">
  <p>Custom Styled</p>
</button>
---
import { Button } from '@/components/ui/button';

---

<Button class="bg-gradient-to-r from-purple-500 to-pink-500 text-white font-bold py-2 px-4 rounded-full shadow-lg hover:shadow-xl transition-shadow duration-300">
Custom Styled
</Button>
---
// Imports
import { type HTMLAttributes } from "astro/types";
import { tv, type VariantProps } from "@utils/custom-tv";
import Icon from "./Icon.astro";
import {
  getDefaultClasses,
  getOutlinedClasses,
  getSoftClasses,
  type ColorName,
  colorPalette,
  getHardTextClass,
  getSoftTextClass,
  getGhostClasses,
  getButtonClasses,
} from "@utils/colorUtils";
import { twMerge } from "tailwind-merge";

// PropTypes for documentation
export const propTypes = {
  variant: {
    type: ["solid", "outline", "ghost", "soft", "text"],
    description: "The variant of the button",
    default: "solid",
  },
  size: {
    type: ["xs", "sm", "md", "lg", "xl", "2xl"],
    description: "The size of the button",
    default: "md",
  },
  color: {
    type: Object.keys(colorPalette),
    description: "The color scheme for the button",
    default: "blue",
  },
  shape: {
    type: ["square", "rounded", "pill"],
    description: "The shape of the button",
    default: "rounded",
  },
  fullWidth: {
    type: "boolean",
    description: "Whether the button should take full width",
    default: false,
  },
  icon: {
    type: "string | { name: string; class?: string }",
    description: "The icon to display in the button",
  },
  rightIcon: {
    type: "string | { name: string; class?: string }",
    description: "The icon to display on the right side of the button",
  },
  iconOnly: {
    type: "boolean",
    description: "Whether the button should only display an icon",
    default: false,
  },
  disabled: {
    type: "boolean",
    description: "Whether the button is disabled",
    default: false,
  },
  class: {
    type: "string",
    description: "Additional CSS classes to apply to the button",
  },
  as: {
    type: ["button", "a"],
    description: "The HTML element to render the button as",
    default: "button",
  },
  href: {
    type: "string",
    description: "The URL to link to when using an anchor tag",
  },
};

// Types and Interfaces
type ButtonVariants = VariantProps<typeof buttonStyles>;

interface Props extends HTMLAttributes<"button" | "a">, ButtonVariants {
  variant?: "solid" | "outline" | "ghost" | "soft" | "text";
  size?: "xs" | "sm" | "md" | "lg" | "xl" | "2xl";
  color?: ColorName;
  shape?: "square" | "rounded" | "pill";
  fullWidth?: boolean;
  icon?: string | { name: string; class?: string };
  rightIcon?: string | { name: string; class?: string };
  iconOnly?: boolean;
  disabled?: boolean;
  class?: string;
  as?: "button" | "a";
  href?: string;
}

// Component Logic
const {
  variant = "solid",
  size = "md",
  color = "blue",
  shape = "rounded",
  fullWidth = false,
  icon,
  rightIcon,
  iconOnly = false,
  disabled = false,
  class: className = "",
  as = "button",
  href,
  ...rest
} = Astro.props as Props;

const normalizeIcon = (icon: Props["icon"]) =>
  typeof icon === "string" ? { name: icon, class: "" } : icon;

const finalIcon = icon ? normalizeIcon(icon) : null;
const finalRightIcon = rightIcon ? normalizeIcon(rightIcon) : null;

// Styles
const buttonStyles = tv({
  base: "inline-flex items-center justify-center font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2",
  variants: {
    variant: {
      solid: getButtonClasses(color),
      outline: `${getOutlinedClasses(color)} border`,
      ghost: `${getGhostClasses(color)}`,
      soft: getSoftClasses(color),
      text: getSoftTextClass(color) + " hover:opacity-70",
    },
    size: {
      xs: "px-2 py-1 text-xs",
      sm: "px-2 py-1 text-sm",
      md: "px-3 py-2 text-sm",
      lg: "px-4 py-2 text-base",
      xl: "px-5 py-3 text-lg",
      "2xl": "px-6 py-4 text-xl",
    },
    shape: {
      square: "",
      rounded: "rounded-md",
      pill: "rounded-full",
    },
    fullWidth: {
      true: "w-full",
      false: "",
    },
    iconOnly: {
      true: "p-2",
      false: "",
    },
    disabled: {
      true: "opacity-50 cursor-not-allowed",
      false: "",
    },
  },

  defaultVariants: {
    variant: "solid",
    size: "md",
    shape: "rounded",
    fullWidth: false,
    iconOnly: false,
    disabled: false,
  },
});

const iconSize =
  size === "xs" || size === "sm"
    ? "w-4 h-4"
    : size === "2xl"
      ? "w-6 h-6"
      : "w-5 h-5";
const Element = as;
---

<Element
  class={twMerge(
    buttonStyles({
      variant,
      size,
      color,
      shape,
      fullWidth,
      iconOnly,
      disabled,
    }),
    className,
  )}
  disabled={as === "button" ? disabled : undefined}
  aria-disabled={disabled}
  href={as === "a" ? href : undefined}
  {...rest}
>
  {
    finalIcon && (
      <Icon
        name={finalIcon.name}
        class={twMerge(iconSize, !iconOnly && "mr-2", finalIcon.class)}
        aria-hidden="true"
      />
    )
  }
  {!iconOnly && <slot />}
  {
    finalRightIcon && (
      <Icon
        name={finalRightIcon.name}
        class={twMerge(iconSize, "ml-2", finalRightIcon.class)}
        aria-hidden="true"
      />
    )
  }
</Element>

Gradient Buttons

Buttons with gradient backgrounds using custom classes.

 
<div class="space-x-2">
  <button
    class="inline-flex items-center justify-center font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 bg-blue-500 hover:bg-blue-600 dark:bg-blue-600 dark:hover:bg-blue-500 text-white dark:text-white hover:text-blue-100 dark:hover:text-blue-200 px-3 py-2 text-sm rounded-md bg-gradient-to-r from-cyan-500 to-blue-500">
    Ocean
  </button>
  <button
    class="inline-flex items-center justify-center font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 bg-blue-500 hover:bg-blue-600 dark:bg-blue-600 dark:hover:bg-blue-500 text-white dark:text-white hover:text-blue-100 dark:hover:text-blue-200 px-3 py-2 text-sm rounded-md bg-gradient-to-r from-purple-500 to-pink-500">
    Sunset
  </button>
  <button
    class="inline-flex items-center justify-center font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 bg-blue-500 hover:bg-blue-600 dark:bg-blue-600 dark:hover:bg-blue-500 text-white dark:text-white hover:text-blue-100 dark:hover:text-blue-200 px-3 py-2 text-sm rounded-md bg-gradient-to-r from-green-400 to-blue-500">
    Nature
  </button>
</div>
---
import { Button } from '@/components/ui/button';

---

<Button class="bg-gradient-to-r from-cyan-500 to-blue-500">Ocean</Button>
<Button class="bg-gradient-to-r from-purple-500 to-pink-500">Sunset</Button>
<Button class="bg-gradient-to-r from-green-400 to-blue-500">Nature</Button>
---
// Imports
import { type HTMLAttributes } from "astro/types";
import { tv, type VariantProps } from "@utils/custom-tv";
import Icon from "./Icon.astro";
import {
  getDefaultClasses,
  getOutlinedClasses,
  getSoftClasses,
  type ColorName,
  colorPalette,
  getHardTextClass,
  getSoftTextClass,
  getGhostClasses,
  getButtonClasses,
} from "@utils/colorUtils";
import { twMerge } from "tailwind-merge";

// PropTypes for documentation
export const propTypes = {
  variant: {
    type: ["solid", "outline", "ghost", "soft", "text"],
    description: "The variant of the button",
    default: "solid",
  },
  size: {
    type: ["xs", "sm", "md", "lg", "xl", "2xl"],
    description: "The size of the button",
    default: "md",
  },
  color: {
    type: Object.keys(colorPalette),
    description: "The color scheme for the button",
    default: "blue",
  },
  shape: {
    type: ["square", "rounded", "pill"],
    description: "The shape of the button",
    default: "rounded",
  },
  fullWidth: {
    type: "boolean",
    description: "Whether the button should take full width",
    default: false,
  },
  icon: {
    type: "string | { name: string; class?: string }",
    description: "The icon to display in the button",
  },
  rightIcon: {
    type: "string | { name: string; class?: string }",
    description: "The icon to display on the right side of the button",
  },
  iconOnly: {
    type: "boolean",
    description: "Whether the button should only display an icon",
    default: false,
  },
  disabled: {
    type: "boolean",
    description: "Whether the button is disabled",
    default: false,
  },
  class: {
    type: "string",
    description: "Additional CSS classes to apply to the button",
  },
  as: {
    type: ["button", "a"],
    description: "The HTML element to render the button as",
    default: "button",
  },
  href: {
    type: "string",
    description: "The URL to link to when using an anchor tag",
  },
};

// Types and Interfaces
type ButtonVariants = VariantProps<typeof buttonStyles>;

interface Props extends HTMLAttributes<"button" | "a">, ButtonVariants {
  variant?: "solid" | "outline" | "ghost" | "soft" | "text";
  size?: "xs" | "sm" | "md" | "lg" | "xl" | "2xl";
  color?: ColorName;
  shape?: "square" | "rounded" | "pill";
  fullWidth?: boolean;
  icon?: string | { name: string; class?: string };
  rightIcon?: string | { name: string; class?: string };
  iconOnly?: boolean;
  disabled?: boolean;
  class?: string;
  as?: "button" | "a";
  href?: string;
}

// Component Logic
const {
  variant = "solid",
  size = "md",
  color = "blue",
  shape = "rounded",
  fullWidth = false,
  icon,
  rightIcon,
  iconOnly = false,
  disabled = false,
  class: className = "",
  as = "button",
  href,
  ...rest
} = Astro.props as Props;

const normalizeIcon = (icon: Props["icon"]) =>
  typeof icon === "string" ? { name: icon, class: "" } : icon;

const finalIcon = icon ? normalizeIcon(icon) : null;
const finalRightIcon = rightIcon ? normalizeIcon(rightIcon) : null;

// Styles
const buttonStyles = tv({
  base: "inline-flex items-center justify-center font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2",
  variants: {
    variant: {
      solid: getButtonClasses(color),
      outline: `${getOutlinedClasses(color)} border`,
      ghost: `${getGhostClasses(color)}`,
      soft: getSoftClasses(color),
      text: getSoftTextClass(color) + " hover:opacity-70",
    },
    size: {
      xs: "px-2 py-1 text-xs",
      sm: "px-2 py-1 text-sm",
      md: "px-3 py-2 text-sm",
      lg: "px-4 py-2 text-base",
      xl: "px-5 py-3 text-lg",
      "2xl": "px-6 py-4 text-xl",
    },
    shape: {
      square: "",
      rounded: "rounded-md",
      pill: "rounded-full",
    },
    fullWidth: {
      true: "w-full",
      false: "",
    },
    iconOnly: {
      true: "p-2",
      false: "",
    },
    disabled: {
      true: "opacity-50 cursor-not-allowed",
      false: "",
    },
  },

  defaultVariants: {
    variant: "solid",
    size: "md",
    shape: "rounded",
    fullWidth: false,
    iconOnly: false,
    disabled: false,
  },
});

const iconSize =
  size === "xs" || size === "sm"
    ? "w-4 h-4"
    : size === "2xl"
      ? "w-6 h-6"
      : "w-5 h-5";
const Element = as;
---

<Element
  class={twMerge(
    buttonStyles({
      variant,
      size,
      color,
      shape,
      fullWidth,
      iconOnly,
      disabled,
    }),
    className,
  )}
  disabled={as === "button" ? disabled : undefined}
  aria-disabled={disabled}
  href={as === "a" ? href : undefined}
  {...rest}
>
  {
    finalIcon && (
      <Icon
        name={finalIcon.name}
        class={twMerge(iconSize, !iconOnly && "mr-2", finalIcon.class)}
        aria-hidden="true"
      />
    )
  }
  {!iconOnly && <slot />}
  {
    finalRightIcon && (
      <Icon
        name={finalRightIcon.name}
        class={twMerge(iconSize, "ml-2", finalRightIcon.class)}
        aria-hidden="true"
      />
    )
  }
</Element>

Animated Buttons

Buttons with hover animations using custom classes.

 
<div class="space-x-2">
  <button
    class="inline-flex items-center justify-center font-medium focus:outline-none focus:ring-2 focus:ring-offset-2 bg-blue-500 hover:bg-blue-600 dark:bg-blue-600 dark:hover:bg-blue-500 text-white dark:text-white hover:text-blue-100 dark:hover:text-blue-200 px-3 py-2 text-sm rounded-md transition-transform duration-300 hover:scale-110">
    Scale
  </button>
  <button
    class="inline-flex items-center justify-center font-medium focus:outline-none focus:ring-2 focus:ring-offset-2 bg-blue-500 hover:bg-blue-600 dark:bg-blue-600 dark:hover:bg-blue-500 text-white dark:text-white hover:text-blue-100 dark:hover:text-blue-200 px-3 py-2 text-sm rounded-md transition-all duration-300 hover:rotate-6 hover:shadow-lg">
    Rotate
  </button>
  <button
    class="inline-flex items-center justify-center font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 bg-blue-500 hover:bg-blue-600 dark:bg-blue-600 dark:hover:bg-blue-500 text-white dark:text-white hover:text-blue-100 dark:hover:text-blue-200 px-3 py-2 text-sm rounded-md relative overflow-hidden group">
    <span class="relative z-10">Slide</span>
    <span
      class="absolute inset-0 bg-purple-600 transform -translate-x-full group-hover:translate-x-0 transition-transform duration-300"></span>
  </button>
</div>
---
import { Button } from '@/components/ui/button';

---

<Button class="transition-transform duration-300 hover:scale-110">Scale</Button>
<Button class="transition-all duration-300 hover:rotate-6 hover:shadow-lg">Rotate</Button>
<Button class="relative overflow-hidden group">
<span class="relative z-10">Slide</span>
<span class="absolute inset-0 bg-purple-600 transform -translate-x-full group-hover:translate-x-0 transition-transform duration-300"></span>
</Button>
---
// Imports
import { type HTMLAttributes } from "astro/types";
import { tv, type VariantProps } from "@utils/custom-tv";
import Icon from "./Icon.astro";
import {
  getDefaultClasses,
  getOutlinedClasses,
  getSoftClasses,
  type ColorName,
  colorPalette,
  getHardTextClass,
  getSoftTextClass,
  getGhostClasses,
  getButtonClasses,
} from "@utils/colorUtils";
import { twMerge } from "tailwind-merge";

// PropTypes for documentation
export const propTypes = {
  variant: {
    type: ["solid", "outline", "ghost", "soft", "text"],
    description: "The variant of the button",
    default: "solid",
  },
  size: {
    type: ["xs", "sm", "md", "lg", "xl", "2xl"],
    description: "The size of the button",
    default: "md",
  },
  color: {
    type: Object.keys(colorPalette),
    description: "The color scheme for the button",
    default: "blue",
  },
  shape: {
    type: ["square", "rounded", "pill"],
    description: "The shape of the button",
    default: "rounded",
  },
  fullWidth: {
    type: "boolean",
    description: "Whether the button should take full width",
    default: false,
  },
  icon: {
    type: "string | { name: string; class?: string }",
    description: "The icon to display in the button",
  },
  rightIcon: {
    type: "string | { name: string; class?: string }",
    description: "The icon to display on the right side of the button",
  },
  iconOnly: {
    type: "boolean",
    description: "Whether the button should only display an icon",
    default: false,
  },
  disabled: {
    type: "boolean",
    description: "Whether the button is disabled",
    default: false,
  },
  class: {
    type: "string",
    description: "Additional CSS classes to apply to the button",
  },
  as: {
    type: ["button", "a"],
    description: "The HTML element to render the button as",
    default: "button",
  },
  href: {
    type: "string",
    description: "The URL to link to when using an anchor tag",
  },
};

// Types and Interfaces
type ButtonVariants = VariantProps<typeof buttonStyles>;

interface Props extends HTMLAttributes<"button" | "a">, ButtonVariants {
  variant?: "solid" | "outline" | "ghost" | "soft" | "text";
  size?: "xs" | "sm" | "md" | "lg" | "xl" | "2xl";
  color?: ColorName;
  shape?: "square" | "rounded" | "pill";
  fullWidth?: boolean;
  icon?: string | { name: string; class?: string };
  rightIcon?: string | { name: string; class?: string };
  iconOnly?: boolean;
  disabled?: boolean;
  class?: string;
  as?: "button" | "a";
  href?: string;
}

// Component Logic
const {
  variant = "solid",
  size = "md",
  color = "blue",
  shape = "rounded",
  fullWidth = false,
  icon,
  rightIcon,
  iconOnly = false,
  disabled = false,
  class: className = "",
  as = "button",
  href,
  ...rest
} = Astro.props as Props;

const normalizeIcon = (icon: Props["icon"]) =>
  typeof icon === "string" ? { name: icon, class: "" } : icon;

const finalIcon = icon ? normalizeIcon(icon) : null;
const finalRightIcon = rightIcon ? normalizeIcon(rightIcon) : null;

// Styles
const buttonStyles = tv({
  base: "inline-flex items-center justify-center font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2",
  variants: {
    variant: {
      solid: getButtonClasses(color),
      outline: `${getOutlinedClasses(color)} border`,
      ghost: `${getGhostClasses(color)}`,
      soft: getSoftClasses(color),
      text: getSoftTextClass(color) + " hover:opacity-70",
    },
    size: {
      xs: "px-2 py-1 text-xs",
      sm: "px-2 py-1 text-sm",
      md: "px-3 py-2 text-sm",
      lg: "px-4 py-2 text-base",
      xl: "px-5 py-3 text-lg",
      "2xl": "px-6 py-4 text-xl",
    },
    shape: {
      square: "",
      rounded: "rounded-md",
      pill: "rounded-full",
    },
    fullWidth: {
      true: "w-full",
      false: "",
    },
    iconOnly: {
      true: "p-2",
      false: "",
    },
    disabled: {
      true: "opacity-50 cursor-not-allowed",
      false: "",
    },
  },

  defaultVariants: {
    variant: "solid",
    size: "md",
    shape: "rounded",
    fullWidth: false,
    iconOnly: false,
    disabled: false,
  },
});

const iconSize =
  size === "xs" || size === "sm"
    ? "w-4 h-4"
    : size === "2xl"
      ? "w-6 h-6"
      : "w-5 h-5";
const Element = as;
---

<Element
  class={twMerge(
    buttonStyles({
      variant,
      size,
      color,
      shape,
      fullWidth,
      iconOnly,
      disabled,
    }),
    className,
  )}
  disabled={as === "button" ? disabled : undefined}
  aria-disabled={disabled}
  href={as === "a" ? href : undefined}
  {...rest}
>
  {
    finalIcon && (
      <Icon
        name={finalIcon.name}
        class={twMerge(iconSize, !iconOnly && "mr-2", finalIcon.class)}
        aria-hidden="true"
      />
    )
  }
  {!iconOnly && <slot />}
  {
    finalRightIcon && (
      <Icon
        name={finalRightIcon.name}
        class={twMerge(iconSize, "ml-2", finalRightIcon.class)}
        aria-hidden="true"
      />
    )
  }
</Element>

Button Groups

Grouped buttons for related actions.

 
<div class="inline-flex rounded-md shadow-sm" role="group">
  <button
    class="inline-flex items-center justify-center font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 bg-blue-500 hover:bg-blue-600 dark:bg-blue-600 dark:hover:bg-blue-500 text-white dark:text-white hover:text-blue-100 dark:hover:text-blue-200 px-3 py-2 text-sm rounded-md rounded-l-lg rounded-r-none border-r border-blue-600">
    Left
  </button>
  <button
    class="inline-flex items-center justify-center font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 bg-blue-500 hover:bg-blue-600 dark:bg-blue-600 dark:hover:bg-blue-500 text-white dark:text-white hover:text-blue-100 dark:hover:text-blue-200 px-3 py-2 text-sm rounded-none border-r border-blue-600">
    Middle
  </button>
  <button
    class="inline-flex items-center justify-center font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 bg-blue-500 hover:bg-blue-600 dark:bg-blue-600 dark:hover:bg-blue-500 text-white dark:text-white hover:text-blue-100 dark:hover:text-blue-200 px-3 py-2 text-sm rounded-md rounded-r-lg rounded-l-none">
    Right
  </button>
</div>
---
import { Button } from '@/components/ui/button';

---

<div class="inline-flex rounded-md shadow-sm" role="group">
<Button class="rounded-l-lg rounded-r-none border-r border-blue-600">Left</Button>
<Button class="rounded-none border-r border-blue-600">Middle</Button>
<Button class="rounded-r-lg rounded-l-none">Right</Button>
</div>
---
// Imports
import { type HTMLAttributes } from "astro/types";
import { tv, type VariantProps } from "@utils/custom-tv";
import Icon from "./Icon.astro";
import {
  getDefaultClasses,
  getOutlinedClasses,
  getSoftClasses,
  type ColorName,
  colorPalette,
  getHardTextClass,
  getSoftTextClass,
  getGhostClasses,
  getButtonClasses,
} from "@utils/colorUtils";
import { twMerge } from "tailwind-merge";

// PropTypes for documentation
export const propTypes = {
  variant: {
    type: ["solid", "outline", "ghost", "soft", "text"],
    description: "The variant of the button",
    default: "solid",
  },
  size: {
    type: ["xs", "sm", "md", "lg", "xl", "2xl"],
    description: "The size of the button",
    default: "md",
  },
  color: {
    type: Object.keys(colorPalette),
    description: "The color scheme for the button",
    default: "blue",
  },
  shape: {
    type: ["square", "rounded", "pill"],
    description: "The shape of the button",
    default: "rounded",
  },
  fullWidth: {
    type: "boolean",
    description: "Whether the button should take full width",
    default: false,
  },
  icon: {
    type: "string | { name: string; class?: string }",
    description: "The icon to display in the button",
  },
  rightIcon: {
    type: "string | { name: string; class?: string }",
    description: "The icon to display on the right side of the button",
  },
  iconOnly: {
    type: "boolean",
    description: "Whether the button should only display an icon",
    default: false,
  },
  disabled: {
    type: "boolean",
    description: "Whether the button is disabled",
    default: false,
  },
  class: {
    type: "string",
    description: "Additional CSS classes to apply to the button",
  },
  as: {
    type: ["button", "a"],
    description: "The HTML element to render the button as",
    default: "button",
  },
  href: {
    type: "string",
    description: "The URL to link to when using an anchor tag",
  },
};

// Types and Interfaces
type ButtonVariants = VariantProps<typeof buttonStyles>;

interface Props extends HTMLAttributes<"button" | "a">, ButtonVariants {
  variant?: "solid" | "outline" | "ghost" | "soft" | "text";
  size?: "xs" | "sm" | "md" | "lg" | "xl" | "2xl";
  color?: ColorName;
  shape?: "square" | "rounded" | "pill";
  fullWidth?: boolean;
  icon?: string | { name: string; class?: string };
  rightIcon?: string | { name: string; class?: string };
  iconOnly?: boolean;
  disabled?: boolean;
  class?: string;
  as?: "button" | "a";
  href?: string;
}

// Component Logic
const {
  variant = "solid",
  size = "md",
  color = "blue",
  shape = "rounded",
  fullWidth = false,
  icon,
  rightIcon,
  iconOnly = false,
  disabled = false,
  class: className = "",
  as = "button",
  href,
  ...rest
} = Astro.props as Props;

const normalizeIcon = (icon: Props["icon"]) =>
  typeof icon === "string" ? { name: icon, class: "" } : icon;

const finalIcon = icon ? normalizeIcon(icon) : null;
const finalRightIcon = rightIcon ? normalizeIcon(rightIcon) : null;

// Styles
const buttonStyles = tv({
  base: "inline-flex items-center justify-center font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2",
  variants: {
    variant: {
      solid: getButtonClasses(color),
      outline: `${getOutlinedClasses(color)} border`,
      ghost: `${getGhostClasses(color)}`,
      soft: getSoftClasses(color),
      text: getSoftTextClass(color) + " hover:opacity-70",
    },
    size: {
      xs: "px-2 py-1 text-xs",
      sm: "px-2 py-1 text-sm",
      md: "px-3 py-2 text-sm",
      lg: "px-4 py-2 text-base",
      xl: "px-5 py-3 text-lg",
      "2xl": "px-6 py-4 text-xl",
    },
    shape: {
      square: "",
      rounded: "rounded-md",
      pill: "rounded-full",
    },
    fullWidth: {
      true: "w-full",
      false: "",
    },
    iconOnly: {
      true: "p-2",
      false: "",
    },
    disabled: {
      true: "opacity-50 cursor-not-allowed",
      false: "",
    },
  },

  defaultVariants: {
    variant: "solid",
    size: "md",
    shape: "rounded",
    fullWidth: false,
    iconOnly: false,
    disabled: false,
  },
});

const iconSize =
  size === "xs" || size === "sm"
    ? "w-4 h-4"
    : size === "2xl"
      ? "w-6 h-6"
      : "w-5 h-5";
const Element = as;
---

<Element
  class={twMerge(
    buttonStyles({
      variant,
      size,
      color,
      shape,
      fullWidth,
      iconOnly,
      disabled,
    }),
    className,
  )}
  disabled={as === "button" ? disabled : undefined}
  aria-disabled={disabled}
  href={as === "a" ? href : undefined}
  {...rest}
>
  {
    finalIcon && (
      <Icon
        name={finalIcon.name}
        class={twMerge(iconSize, !iconOnly && "mr-2", finalIcon.class)}
        aria-hidden="true"
      />
    )
  }
  {!iconOnly && <slot />}
  {
    finalRightIcon && (
      <Icon
        name={finalRightIcon.name}
        class={twMerge(iconSize, "ml-2", finalRightIcon.class)}
        aria-hidden="true"
      />
    )
  }
</Element>

Loading Button

A button with a loading state using an icon.

 
<button
  class="inline-flex items-center justify-center font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 bg-blue-500 hover:bg-blue-600 dark:bg-blue-600 dark:hover:bg-blue-500 text-white dark:text-white hover:text-blue-100 dark:hover:text-blue-200 px-3 py-2 text-sm rounded-md opacity-50 cursor-wait"
  disabled
  aria-disabled="true">
  <span class="mr-2">
    <span class="inline-block size-5 w-4 h-4 animate-spin">
      <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="M4.755 10.059a7.5 7.5 0 0 1 12.548-3.364l1.903 1.903h-3.183a.75.75 0 1 0 0 1.5h4.992a.75.75 0 0 0 .75-.75V4.356a.75.75 0 0 0-1.5 0v3.18l-1.9-1.9A9 9 0 0 0 3.306 9.67a.75.75 0 1 0 1.45.388Zm15.408 3.352a.75.75 0 0 0-.919.53 7.5 7.5 0 0 1-12.548 3.364l-1.902-1.903h3.183a.75.75 0 0 0 0-1.5H2.984a.75.75 0 0 0-.75.75v4.992a.75.75 0 0 0 1.5 0v-3.18l1.9 1.9a9 9 0 0 0 15.059-4.035.75.75 0 0 0-.53-.918Z"
          clip-rule="evenodd"></path>
      </svg>
    </span>
  </span>
  <p>Loading…</p>
</button>
---
import { Button } from '@/components/ui/button';
import { Icon } from '@/components/ui/icon';

---

<Button disabled class="cursor-wait">
<span class="mr-2">
<Icon name="ArrowPathIcon" class="w-4 h-4 animate-spin" />
</span>
Loading...
</Button>
---
// Imports
import { type HTMLAttributes } from "astro/types";
import { tv, type VariantProps } from "@utils/custom-tv";
import Icon from "./Icon.astro";
import {
  getDefaultClasses,
  getOutlinedClasses,
  getSoftClasses,
  type ColorName,
  colorPalette,
  getHardTextClass,
  getSoftTextClass,
  getGhostClasses,
  getButtonClasses,
} from "@utils/colorUtils";
import { twMerge } from "tailwind-merge";

// PropTypes for documentation
export const propTypes = {
  variant: {
    type: ["solid", "outline", "ghost", "soft", "text"],
    description: "The variant of the button",
    default: "solid",
  },
  size: {
    type: ["xs", "sm", "md", "lg", "xl", "2xl"],
    description: "The size of the button",
    default: "md",
  },
  color: {
    type: Object.keys(colorPalette),
    description: "The color scheme for the button",
    default: "blue",
  },
  shape: {
    type: ["square", "rounded", "pill"],
    description: "The shape of the button",
    default: "rounded",
  },
  fullWidth: {
    type: "boolean",
    description: "Whether the button should take full width",
    default: false,
  },
  icon: {
    type: "string | { name: string; class?: string }",
    description: "The icon to display in the button",
  },
  rightIcon: {
    type: "string | { name: string; class?: string }",
    description: "The icon to display on the right side of the button",
  },
  iconOnly: {
    type: "boolean",
    description: "Whether the button should only display an icon",
    default: false,
  },
  disabled: {
    type: "boolean",
    description: "Whether the button is disabled",
    default: false,
  },
  class: {
    type: "string",
    description: "Additional CSS classes to apply to the button",
  },
  as: {
    type: ["button", "a"],
    description: "The HTML element to render the button as",
    default: "button",
  },
  href: {
    type: "string",
    description: "The URL to link to when using an anchor tag",
  },
};

// Types and Interfaces
type ButtonVariants = VariantProps<typeof buttonStyles>;

interface Props extends HTMLAttributes<"button" | "a">, ButtonVariants {
  variant?: "solid" | "outline" | "ghost" | "soft" | "text";
  size?: "xs" | "sm" | "md" | "lg" | "xl" | "2xl";
  color?: ColorName;
  shape?: "square" | "rounded" | "pill";
  fullWidth?: boolean;
  icon?: string | { name: string; class?: string };
  rightIcon?: string | { name: string; class?: string };
  iconOnly?: boolean;
  disabled?: boolean;
  class?: string;
  as?: "button" | "a";
  href?: string;
}

// Component Logic
const {
  variant = "solid",
  size = "md",
  color = "blue",
  shape = "rounded",
  fullWidth = false,
  icon,
  rightIcon,
  iconOnly = false,
  disabled = false,
  class: className = "",
  as = "button",
  href,
  ...rest
} = Astro.props as Props;

const normalizeIcon = (icon: Props["icon"]) =>
  typeof icon === "string" ? { name: icon, class: "" } : icon;

const finalIcon = icon ? normalizeIcon(icon) : null;
const finalRightIcon = rightIcon ? normalizeIcon(rightIcon) : null;

// Styles
const buttonStyles = tv({
  base: "inline-flex items-center justify-center font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2",
  variants: {
    variant: {
      solid: getButtonClasses(color),
      outline: `${getOutlinedClasses(color)} border`,
      ghost: `${getGhostClasses(color)}`,
      soft: getSoftClasses(color),
      text: getSoftTextClass(color) + " hover:opacity-70",
    },
    size: {
      xs: "px-2 py-1 text-xs",
      sm: "px-2 py-1 text-sm",
      md: "px-3 py-2 text-sm",
      lg: "px-4 py-2 text-base",
      xl: "px-5 py-3 text-lg",
      "2xl": "px-6 py-4 text-xl",
    },
    shape: {
      square: "",
      rounded: "rounded-md",
      pill: "rounded-full",
    },
    fullWidth: {
      true: "w-full",
      false: "",
    },
    iconOnly: {
      true: "p-2",
      false: "",
    },
    disabled: {
      true: "opacity-50 cursor-not-allowed",
      false: "",
    },
  },

  defaultVariants: {
    variant: "solid",
    size: "md",
    shape: "rounded",
    fullWidth: false,
    iconOnly: false,
    disabled: false,
  },
});

const iconSize =
  size === "xs" || size === "sm"
    ? "w-4 h-4"
    : size === "2xl"
      ? "w-6 h-6"
      : "w-5 h-5";
const Element = as;
---

<Element
  class={twMerge(
    buttonStyles({
      variant,
      size,
      color,
      shape,
      fullWidth,
      iconOnly,
      disabled,
    }),
    className,
  )}
  disabled={as === "button" ? disabled : undefined}
  aria-disabled={disabled}
  href={as === "a" ? href : undefined}
  {...rest}
>
  {
    finalIcon && (
      <Icon
        name={finalIcon.name}
        class={twMerge(iconSize, !iconOnly && "mr-2", finalIcon.class)}
        aria-hidden="true"
      />
    )
  }
  {!iconOnly && <slot />}
  {
    finalRightIcon && (
      <Icon
        name={finalRightIcon.name}
        class={twMerge(iconSize, "ml-2", finalRightIcon.class)}
        aria-hidden="true"
      />
    )
  }
</Element>

Button as Anchor

A button rendered as an anchor tag using the 'as' prop.

 
<a
  class="inline-flex items-center justify-center font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 bg-blue-500 hover:bg-blue-600 dark:bg-blue-600 dark:hover:bg-blue-500 text-white dark:text-white hover:text-blue-100 dark:hover:text-blue-200 px-3 py-2 text-sm rounded-md"
  href="https://example.com"
  target="_blank"
  rel="noopener noreferrer">
  <p>Visit Example</p>
</a>
---
import { Button } from '@/components/ui/button';

---

<Button as="a" href="https://example.com" target="_blank" rel="noopener noreferrer">
Visit Example
</Button>
---
// Imports
import { type HTMLAttributes } from "astro/types";
import { tv, type VariantProps } from "@utils/custom-tv";
import Icon from "./Icon.astro";
import {
  getDefaultClasses,
  getOutlinedClasses,
  getSoftClasses,
  type ColorName,
  colorPalette,
  getHardTextClass,
  getSoftTextClass,
  getGhostClasses,
  getButtonClasses,
} from "@utils/colorUtils";
import { twMerge } from "tailwind-merge";

// PropTypes for documentation
export const propTypes = {
  variant: {
    type: ["solid", "outline", "ghost", "soft", "text"],
    description: "The variant of the button",
    default: "solid",
  },
  size: {
    type: ["xs", "sm", "md", "lg", "xl", "2xl"],
    description: "The size of the button",
    default: "md",
  },
  color: {
    type: Object.keys(colorPalette),
    description: "The color scheme for the button",
    default: "blue",
  },
  shape: {
    type: ["square", "rounded", "pill"],
    description: "The shape of the button",
    default: "rounded",
  },
  fullWidth: {
    type: "boolean",
    description: "Whether the button should take full width",
    default: false,
  },
  icon: {
    type: "string | { name: string; class?: string }",
    description: "The icon to display in the button",
  },
  rightIcon: {
    type: "string | { name: string; class?: string }",
    description: "The icon to display on the right side of the button",
  },
  iconOnly: {
    type: "boolean",
    description: "Whether the button should only display an icon",
    default: false,
  },
  disabled: {
    type: "boolean",
    description: "Whether the button is disabled",
    default: false,
  },
  class: {
    type: "string",
    description: "Additional CSS classes to apply to the button",
  },
  as: {
    type: ["button", "a"],
    description: "The HTML element to render the button as",
    default: "button",
  },
  href: {
    type: "string",
    description: "The URL to link to when using an anchor tag",
  },
};

// Types and Interfaces
type ButtonVariants = VariantProps<typeof buttonStyles>;

interface Props extends HTMLAttributes<"button" | "a">, ButtonVariants {
  variant?: "solid" | "outline" | "ghost" | "soft" | "text";
  size?: "xs" | "sm" | "md" | "lg" | "xl" | "2xl";
  color?: ColorName;
  shape?: "square" | "rounded" | "pill";
  fullWidth?: boolean;
  icon?: string | { name: string; class?: string };
  rightIcon?: string | { name: string; class?: string };
  iconOnly?: boolean;
  disabled?: boolean;
  class?: string;
  as?: "button" | "a";
  href?: string;
}

// Component Logic
const {
  variant = "solid",
  size = "md",
  color = "blue",
  shape = "rounded",
  fullWidth = false,
  icon,
  rightIcon,
  iconOnly = false,
  disabled = false,
  class: className = "",
  as = "button",
  href,
  ...rest
} = Astro.props as Props;

const normalizeIcon = (icon: Props["icon"]) =>
  typeof icon === "string" ? { name: icon, class: "" } : icon;

const finalIcon = icon ? normalizeIcon(icon) : null;
const finalRightIcon = rightIcon ? normalizeIcon(rightIcon) : null;

// Styles
const buttonStyles = tv({
  base: "inline-flex items-center justify-center font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2",
  variants: {
    variant: {
      solid: getButtonClasses(color),
      outline: `${getOutlinedClasses(color)} border`,
      ghost: `${getGhostClasses(color)}`,
      soft: getSoftClasses(color),
      text: getSoftTextClass(color) + " hover:opacity-70",
    },
    size: {
      xs: "px-2 py-1 text-xs",
      sm: "px-2 py-1 text-sm",
      md: "px-3 py-2 text-sm",
      lg: "px-4 py-2 text-base",
      xl: "px-5 py-3 text-lg",
      "2xl": "px-6 py-4 text-xl",
    },
    shape: {
      square: "",
      rounded: "rounded-md",
      pill: "rounded-full",
    },
    fullWidth: {
      true: "w-full",
      false: "",
    },
    iconOnly: {
      true: "p-2",
      false: "",
    },
    disabled: {
      true: "opacity-50 cursor-not-allowed",
      false: "",
    },
  },

  defaultVariants: {
    variant: "solid",
    size: "md",
    shape: "rounded",
    fullWidth: false,
    iconOnly: false,
    disabled: false,
  },
});

const iconSize =
  size === "xs" || size === "sm"
    ? "w-4 h-4"
    : size === "2xl"
      ? "w-6 h-6"
      : "w-5 h-5";
const Element = as;
---

<Element
  class={twMerge(
    buttonStyles({
      variant,
      size,
      color,
      shape,
      fullWidth,
      iconOnly,
      disabled,
    }),
    className,
  )}
  disabled={as === "button" ? disabled : undefined}
  aria-disabled={disabled}
  href={as === "a" ? href : undefined}
  {...rest}
>
  {
    finalIcon && (
      <Icon
        name={finalIcon.name}
        class={twMerge(iconSize, !iconOnly && "mr-2", finalIcon.class)}
        aria-hidden="true"
      />
    )
  }
  {!iconOnly && <slot />}
  {
    finalRightIcon && (
      <Icon
        name={finalRightIcon.name}
        class={twMerge(iconSize, "ml-2", finalRightIcon.class)}
        aria-hidden="true"
      />
    )
  }
</Element>

Button vs Anchor Comparison

Comparison between a regular button and a button rendered as an anchor.

 
<div class="space-x-2">
  <button
    class="inline-flex items-center justify-center font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 bg-blue-500 hover:bg-blue-600 dark:bg-blue-600 dark:hover:bg-blue-500 text-white dark:text-white hover:text-blue-100 dark:hover:text-blue-200 px-3 py-2 text-sm rounded-md">
    Regular Button
  </button>
  <a
    class="inline-flex items-center justify-center font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 bg-blue-500 hover:bg-blue-600 dark:bg-blue-600 dark:hover:bg-blue-500 text-white dark:text-white hover:text-blue-100 dark:hover:text-blue-200 px-3 py-2 text-sm rounded-md ml-2"
    href="#">
    Anchor Button
  </a>
</div>
---
import { Button } from '@/components/ui/button';

---

<Button>Regular Button</Button>
<Button as="a" href="#" class="ml-2">Anchor Button</Button>
---
// Imports
import { type HTMLAttributes } from "astro/types";
import { tv, type VariantProps } from "@utils/custom-tv";
import Icon from "./Icon.astro";
import {
  getDefaultClasses,
  getOutlinedClasses,
  getSoftClasses,
  type ColorName,
  colorPalette,
  getHardTextClass,
  getSoftTextClass,
  getGhostClasses,
  getButtonClasses,
} from "@utils/colorUtils";
import { twMerge } from "tailwind-merge";

// PropTypes for documentation
export const propTypes = {
  variant: {
    type: ["solid", "outline", "ghost", "soft", "text"],
    description: "The variant of the button",
    default: "solid",
  },
  size: {
    type: ["xs", "sm", "md", "lg", "xl", "2xl"],
    description: "The size of the button",
    default: "md",
  },
  color: {
    type: Object.keys(colorPalette),
    description: "The color scheme for the button",
    default: "blue",
  },
  shape: {
    type: ["square", "rounded", "pill"],
    description: "The shape of the button",
    default: "rounded",
  },
  fullWidth: {
    type: "boolean",
    description: "Whether the button should take full width",
    default: false,
  },
  icon: {
    type: "string | { name: string; class?: string }",
    description: "The icon to display in the button",
  },
  rightIcon: {
    type: "string | { name: string; class?: string }",
    description: "The icon to display on the right side of the button",
  },
  iconOnly: {
    type: "boolean",
    description: "Whether the button should only display an icon",
    default: false,
  },
  disabled: {
    type: "boolean",
    description: "Whether the button is disabled",
    default: false,
  },
  class: {
    type: "string",
    description: "Additional CSS classes to apply to the button",
  },
  as: {
    type: ["button", "a"],
    description: "The HTML element to render the button as",
    default: "button",
  },
  href: {
    type: "string",
    description: "The URL to link to when using an anchor tag",
  },
};

// Types and Interfaces
type ButtonVariants = VariantProps<typeof buttonStyles>;

interface Props extends HTMLAttributes<"button" | "a">, ButtonVariants {
  variant?: "solid" | "outline" | "ghost" | "soft" | "text";
  size?: "xs" | "sm" | "md" | "lg" | "xl" | "2xl";
  color?: ColorName;
  shape?: "square" | "rounded" | "pill";
  fullWidth?: boolean;
  icon?: string | { name: string; class?: string };
  rightIcon?: string | { name: string; class?: string };
  iconOnly?: boolean;
  disabled?: boolean;
  class?: string;
  as?: "button" | "a";
  href?: string;
}

// Component Logic
const {
  variant = "solid",
  size = "md",
  color = "blue",
  shape = "rounded",
  fullWidth = false,
  icon,
  rightIcon,
  iconOnly = false,
  disabled = false,
  class: className = "",
  as = "button",
  href,
  ...rest
} = Astro.props as Props;

const normalizeIcon = (icon: Props["icon"]) =>
  typeof icon === "string" ? { name: icon, class: "" } : icon;

const finalIcon = icon ? normalizeIcon(icon) : null;
const finalRightIcon = rightIcon ? normalizeIcon(rightIcon) : null;

// Styles
const buttonStyles = tv({
  base: "inline-flex items-center justify-center font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2",
  variants: {
    variant: {
      solid: getButtonClasses(color),
      outline: `${getOutlinedClasses(color)} border`,
      ghost: `${getGhostClasses(color)}`,
      soft: getSoftClasses(color),
      text: getSoftTextClass(color) + " hover:opacity-70",
    },
    size: {
      xs: "px-2 py-1 text-xs",
      sm: "px-2 py-1 text-sm",
      md: "px-3 py-2 text-sm",
      lg: "px-4 py-2 text-base",
      xl: "px-5 py-3 text-lg",
      "2xl": "px-6 py-4 text-xl",
    },
    shape: {
      square: "",
      rounded: "rounded-md",
      pill: "rounded-full",
    },
    fullWidth: {
      true: "w-full",
      false: "",
    },
    iconOnly: {
      true: "p-2",
      false: "",
    },
    disabled: {
      true: "opacity-50 cursor-not-allowed",
      false: "",
    },
  },

  defaultVariants: {
    variant: "solid",
    size: "md",
    shape: "rounded",
    fullWidth: false,
    iconOnly: false,
    disabled: false,
  },
});

const iconSize =
  size === "xs" || size === "sm"
    ? "w-4 h-4"
    : size === "2xl"
      ? "w-6 h-6"
      : "w-5 h-5";
const Element = as;
---

<Element
  class={twMerge(
    buttonStyles({
      variant,
      size,
      color,
      shape,
      fullWidth,
      iconOnly,
      disabled,
    }),
    className,
  )}
  disabled={as === "button" ? disabled : undefined}
  aria-disabled={disabled}
  href={as === "a" ? href : undefined}
  {...rest}
>
  {
    finalIcon && (
      <Icon
        name={finalIcon.name}
        class={twMerge(iconSize, !iconOnly && "mr-2", finalIcon.class)}
        aria-hidden="true"
      />
    )
  }
  {!iconOnly && <slot />}
  {
    finalRightIcon && (
      <Icon
        name={finalRightIcon.name}
        class={twMerge(iconSize, "ml-2", finalRightIcon.class)}
        aria-hidden="true"
      />
    )
  }
</Element>

Button as Anchor with Icon

A button rendered as an anchor with an icon.

 
<a
  class="inline-flex items-center justify-center font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 bg-blue-500 hover:bg-blue-600 dark:bg-blue-600 dark:hover:bg-blue-500 text-white dark:text-white hover:text-blue-100 dark:hover:text-blue-200 px-3 py-2 text-sm rounded-md"
  href="#">
  <span class="inline-block size-5 w-5 h-5 mr-2">
    <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 12Zm11.378-3.917c-.89-.777-2.366-.777-3.255 0a.75.75 0 0 1-.988-1.129c1.454-1.272 3.776-1.272 5.23 0 1.513 1.324 1.513 3.518 0 4.842a3.75 3.75 0 0 1-.837.552c-.676.328-1.028.774-1.028 1.152v.75a.75.75 0 0 1-1.5 0v-.75c0-1.279 1.06-2.107 1.875-2.502.182-.088.351-.199.503-.331.83-.727.83-1.857 0-2.584ZM12 18a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5Z"
        clip-rule="evenodd"></path>
    </svg>
    <span class="sr-only">Unknown icon</span>
  </span>
  External Link
</a>
---
import { Button } from '@/components/ui/button';

---

<Button as="a" href="#" icon="ExternalLinkIcon">External Link</Button>
---
// Imports
import { type HTMLAttributes } from "astro/types";
import { tv, type VariantProps } from "@utils/custom-tv";
import Icon from "./Icon.astro";
import {
  getDefaultClasses,
  getOutlinedClasses,
  getSoftClasses,
  type ColorName,
  colorPalette,
  getHardTextClass,
  getSoftTextClass,
  getGhostClasses,
  getButtonClasses,
} from "@utils/colorUtils";
import { twMerge } from "tailwind-merge";

// PropTypes for documentation
export const propTypes = {
  variant: {
    type: ["solid", "outline", "ghost", "soft", "text"],
    description: "The variant of the button",
    default: "solid",
  },
  size: {
    type: ["xs", "sm", "md", "lg", "xl", "2xl"],
    description: "The size of the button",
    default: "md",
  },
  color: {
    type: Object.keys(colorPalette),
    description: "The color scheme for the button",
    default: "blue",
  },
  shape: {
    type: ["square", "rounded", "pill"],
    description: "The shape of the button",
    default: "rounded",
  },
  fullWidth: {
    type: "boolean",
    description: "Whether the button should take full width",
    default: false,
  },
  icon: {
    type: "string | { name: string; class?: string }",
    description: "The icon to display in the button",
  },
  rightIcon: {
    type: "string | { name: string; class?: string }",
    description: "The icon to display on the right side of the button",
  },
  iconOnly: {
    type: "boolean",
    description: "Whether the button should only display an icon",
    default: false,
  },
  disabled: {
    type: "boolean",
    description: "Whether the button is disabled",
    default: false,
  },
  class: {
    type: "string",
    description: "Additional CSS classes to apply to the button",
  },
  as: {
    type: ["button", "a"],
    description: "The HTML element to render the button as",
    default: "button",
  },
  href: {
    type: "string",
    description: "The URL to link to when using an anchor tag",
  },
};

// Types and Interfaces
type ButtonVariants = VariantProps<typeof buttonStyles>;

interface Props extends HTMLAttributes<"button" | "a">, ButtonVariants {
  variant?: "solid" | "outline" | "ghost" | "soft" | "text";
  size?: "xs" | "sm" | "md" | "lg" | "xl" | "2xl";
  color?: ColorName;
  shape?: "square" | "rounded" | "pill";
  fullWidth?: boolean;
  icon?: string | { name: string; class?: string };
  rightIcon?: string | { name: string; class?: string };
  iconOnly?: boolean;
  disabled?: boolean;
  class?: string;
  as?: "button" | "a";
  href?: string;
}

// Component Logic
const {
  variant = "solid",
  size = "md",
  color = "blue",
  shape = "rounded",
  fullWidth = false,
  icon,
  rightIcon,
  iconOnly = false,
  disabled = false,
  class: className = "",
  as = "button",
  href,
  ...rest
} = Astro.props as Props;

const normalizeIcon = (icon: Props["icon"]) =>
  typeof icon === "string" ? { name: icon, class: "" } : icon;

const finalIcon = icon ? normalizeIcon(icon) : null;
const finalRightIcon = rightIcon ? normalizeIcon(rightIcon) : null;

// Styles
const buttonStyles = tv({
  base: "inline-flex items-center justify-center font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2",
  variants: {
    variant: {
      solid: getButtonClasses(color),
      outline: `${getOutlinedClasses(color)} border`,
      ghost: `${getGhostClasses(color)}`,
      soft: getSoftClasses(color),
      text: getSoftTextClass(color) + " hover:opacity-70",
    },
    size: {
      xs: "px-2 py-1 text-xs",
      sm: "px-2 py-1 text-sm",
      md: "px-3 py-2 text-sm",
      lg: "px-4 py-2 text-base",
      xl: "px-5 py-3 text-lg",
      "2xl": "px-6 py-4 text-xl",
    },
    shape: {
      square: "",
      rounded: "rounded-md",
      pill: "rounded-full",
    },
    fullWidth: {
      true: "w-full",
      false: "",
    },
    iconOnly: {
      true: "p-2",
      false: "",
    },
    disabled: {
      true: "opacity-50 cursor-not-allowed",
      false: "",
    },
  },

  defaultVariants: {
    variant: "solid",
    size: "md",
    shape: "rounded",
    fullWidth: false,
    iconOnly: false,
    disabled: false,
  },
});

const iconSize =
  size === "xs" || size === "sm"
    ? "w-4 h-4"
    : size === "2xl"
      ? "w-6 h-6"
      : "w-5 h-5";
const Element = as;
---

<Element
  class={twMerge(
    buttonStyles({
      variant,
      size,
      color,
      shape,
      fullWidth,
      iconOnly,
      disabled,
    }),
    className,
  )}
  disabled={as === "button" ? disabled : undefined}
  aria-disabled={disabled}
  href={as === "a" ? href : undefined}
  {...rest}
>
  {
    finalIcon && (
      <Icon
        name={finalIcon.name}
        class={twMerge(iconSize, !iconOnly && "mr-2", finalIcon.class)}
        aria-hidden="true"
      />
    )
  }
  {!iconOnly && <slot />}
  {
    finalRightIcon && (
      <Icon
        name={finalRightIcon.name}
        class={twMerge(iconSize, "ml-2", finalRightIcon.class)}
        aria-hidden="true"
      />
    )
  }
</Element>

Component Properties

Property Type Default Description
variant solid | outline | ghost | soft | text "solid" The variant of the button
size xs | sm | md | lg | xl | 2xl "md" The size of the button
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 button
shape square | rounded | pill "rounded" The shape of the button
fullWidth boolean false Whether the button should take full width
icon string | { name: string; class?: string } - The icon to display in the button
rightIcon string | { name: string; class?: string } - The icon to display on the right side of the button
iconOnly boolean false Whether the button should only display an icon
disabled boolean false Whether the button is disabled
class string - Additional CSS classes to apply to the button
as button | a "button" The HTML element to render the button as
href string - The URL to link to when using an anchor tag

Usage Notes

Button vs Anchor

The Button component can be rendered as either a <button> element (default) or an <a> element using the as prop:

  • Use <Button> for actions that don’t navigate to a new page.
  • Use <Button as="a" href="..."> for actions that navigate to a new page or URL.

When using as="a", make sure to provide an href attribute. Other anchor-specific attributes like target and rel can also be used.