Buttons
Versatile button components with various styles, sizes, and customization options. Supports different variants, colors, shapes, and icon integrations. previews<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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.