Alerts
Informative alert components for displaying messages, warnings, and notifications. Supports various styles, colors, and customization options. previews
<div
class="flex items-start bg-blue-100 dark:bg-blue-800 text-blue-700 dark:text-blue-100 border-blue-200 dark:border-blue-600 border rounded-lg p-4 text-sm"
role="alert"
aria-live="polite">
<div class="flex-1"><p>This is a default alert message.</p></div>
</div>
---
import { Alert } from '@/components/ui/alert';
---
<Alert>
<p>This is a default alert message.</p>
</Alert> ---
// Imports
import { type HTMLAttributes } from "astro/types";
import { tv, type VariantProps } from "@utils/custom-tv";
import Icon from "./Icon.astro";
import {
getOutlinedClasses,
type ColorName,
getDefaultClasses,
getSoftClasses,
colorPalette,
} from "@utils/colorUtils";
import { twMerge } from "tailwind-merge";
// PropTypes for documentation
export const propTypes = {
title: { type: "string", description: "The title of the alert" },
content: {
type: "string | JSX.Element",
description: "The content of the alert",
},
icon: {
type: "string | { name: string; class?: string }",
description: "The icon to display in the alert",
},
rightIcon: {
type: "string | { name: string; class?: string }",
description: "The icon to display on the right side of the alert",
},
iconPosition: {
type: ["left", "right"],
description: "The position of the icon",
default: "left",
},
class: {
type: "string",
description: "Additional CSS classes to apply to the alert",
},
dismissible: {
type: "boolean",
description: "Whether the alert is dismissible",
default: false,
},
variant: {
type: ["filled", "outline", "default"],
description: "The variant of the alert",
default: "default",
},
color: {
type: Object.keys(colorPalette),
description: "The color scheme for the alert",
default: "blue",
},
status: {
type: ["info", "success", "warning", "error"],
description: "The status of the alert",
},
borderAccent: {
type: ["top", "left", "right", "bottom", "none", "square"],
description: "The border accent position or shape",
default: "none",
},
size: {
type: ["sm", "md", "lg"],
description: "The size of the alert",
default: "md",
},
};
// Types and Interfaces
type AlertVariants = VariantProps<typeof alertStyles>;
interface Props extends HTMLAttributes<"div">, AlertVariants {
/**
* The title of the alert.
*/
title?: string;
/**
* The content of the alert.
*/
content?: string | JSX.Element;
/**
* The icon to display in the alert.
*/
icon?: string | { name: string; class?: string };
/**
* The icon to display on the right side of the alert.
*/
rightIcon?: string | { name: string; class?: string };
/**
* The position of the icon.
*/
iconPosition?: "left" | "right";
/**
* Additional CSS classes to apply to the alert.
*/
class?: string;
/**
* Whether the alert is dismissible.
*/
dismissible?: boolean;
/**
* The variant of the alert.
*/
variant?: "filled" | "outline" | "default";
/**
* The color scheme for the alert.
*/
color?: ColorName;
/**
* The status of the alert.
*/
status?: "info" | "success" | "warning" | "error";
/**
* The border accent position or shape.
*/
borderAccent?: "top" | "left" | "right" | "bottom" | "none" | "square";
/**
* The size of the alert.
*/
size?: "sm" | "md" | "lg";
}
// Component Logic
const {
title,
content,
icon,
rightIcon,
iconPosition = "left",
variant = "default",
status,
color = "blue",
dismissible = false,
borderAccent = "none",
class: className = "",
size = "md",
} = Astro.props as Props;
// Styles
const alertStyles = tv({
base: "flex items-start border",
variants: {
variant: {
filled: getDefaultClasses(color, { includeHover: false }),
outline: `${getOutlinedClasses(color, { includeHover: false })} border`,
default: `${getSoftClasses(color, { includeHover: false })} border`,
},
borderAccent: {
top: "rounded-b-lg border-t-4",
left: "rounded-r-lg border-l-4",
right: "rounded-l-lg border-r-4",
bottom: "rounded-t-lg border-b-4",
none: "rounded-lg",
square: "", // No rounded corners
},
size: {
sm: "p-3 text-xs",
md: "p-4 text-sm",
lg: "p-6 text-base",
},
status: {
info: getSoftClasses("blue"),
success: getSoftClasses("green"),
warning: getSoftClasses("yellow"),
error: getSoftClasses("red"),
},
},
defaultVariants: {
variant: "default",
size: "md",
borderAccent: "none",
status,
},
});
const getStatusIcon = (
status: Props["status"],
): { name: string; class: string } => {
const baseClass = "text-current";
switch (status) {
case "info":
return { name: "InformationCircleIcon", class: baseClass };
case "success":
return { name: "CheckCircleIcon", class: baseClass };
case "warning":
return { name: "ExclamationTriangleIcon", class: baseClass };
case "error":
return { name: "XCircleIcon", class: baseClass };
default:
return { name: "InformationCircleIcon", class: baseClass };
}
};
const finalIcon = icon
? typeof icon === "string"
? { name: icon, class: "" }
: icon
: status
? getStatusIcon(status)
: undefined;
const finalRightIcon = rightIcon
? typeof rightIcon === "string"
? { name: rightIcon, class: "" }
: rightIcon
: undefined;
---
<!-- Component Template -->
<div
class={twMerge(
alertStyles({ variant, borderAccent, size, status }),
className,
)}
role="alert"
aria-live="polite"
aria-labelledby={title ? "alert-title" : undefined}
>
{
finalIcon && iconPosition === "left" && (
<Icon
name={finalIcon.name}
class={twMerge(
`mr-3 ${
size === "sm" ? "w-4 h-4" : size === "lg" ? "w-6 h-6" : "w-5 h-5"
}`,
finalIcon.class,
)}
aria-hidden="true"
/>
)
}
<div class="flex-1">
{
title && (
<h3
id="alert-title"
class={twMerge(
"font-semibold -mt-1 mb-1 block",
size === "sm"
? "text-base"
: size === "md"
? "text-base"
: "text-lg",
)}
>
{title}
</h3>
)
}
{
content && (
<p>
<Fragment set:html={content} />
</p>
)
}
<slot />
</div>
{
(finalIcon && iconPosition === "right") || finalRightIcon ? (
<div class="flex items-center ml-3">
{finalIcon && iconPosition === "right" && (
<Icon
name={finalIcon.name}
class={twMerge(
`mt-0.5 ${
size === "sm"
? "w-4 h-4"
: size === "lg"
? "w-6 h-6"
: "w-5 h-5"
}`,
finalIcon.class,
)}
aria-hidden="true"
/>
)}
{finalRightIcon && (
<Icon
name={finalRightIcon.name}
class={twMerge(
`ml-2 mt-0.5 ${
size === "sm"
? "w-4 h-4"
: size === "lg"
? "w-6 h-6"
: "w-5 h-5"
}`,
finalRightIcon.class,
)}
aria-hidden="true"
/>
)}
</div>
) : null
}
{
dismissible && (
<button
type="button"
class={twMerge(
`ml-auto -mx-1.5 -my-1.5 rounded-lg focus:ring-2 p-1.5 inline-flex items-center justify-center transition-colors`,
`focus:ring-${color}-400 hover:bg-${color}-200 hover:text-${color}-900`,
)}
aria-label="Dismiss"
>
<Icon
name="XMarkIcon"
class={twMerge(
size === "sm" ? "w-4 h-4" : size === "lg" ? "w-6 h-6" : "w-5 h-5",
)}
aria-hidden="true"
/>
</button>
)
}
</div>
<div
class="flex items-start bg-blue-100 dark:bg-blue-800 text-blue-700 dark:text-blue-100 border-blue-200 dark:border-blue-600 border rounded-lg p-4 text-sm"
role="alert"
aria-live="polite"
aria-labelledby="alert-title">
<span class="inline-block size-5 mr-3 w-5 h-5">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
aria-hidden="true"
data-slot="icon">
<path
fill-rule="evenodd"
d="M2.25 12c0-5.385 4.365-9.75 9.75-9.75s9.75 4.365 9.75 9.75-4.365 9.75-9.75 9.75S2.25 17.385 2.25 12Zm8.706-1.442c1.146-.573 2.437.463 2.126 1.706l-.709 2.836.042-.02a.75.75 0 0 1 .67 1.34l-.04.022c-1.147.573-2.438-.463-2.127-1.706l.71-2.836-.042.02a.75.75 0 1 1-.671-1.34l.041-.022ZM12 9a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5Z"
clip-rule="evenodd"></path>
</svg>
</span>
<div class="flex-1">
<h3 id="alert-title" class="font-semibold -mt-1 mb-1 block text-base">Information</h3>
<p>This is an alert with a title and icon.</p>
</div>
</div>
---
import { Alert } from '@/components/ui/alert';
---
<Alert title="Information" icon="InformationCircleIcon">
This is an alert with a title and icon.
</Alert> ---
// Imports
import { type HTMLAttributes } from "astro/types";
import { tv, type VariantProps } from "@utils/custom-tv";
import Icon from "./Icon.astro";
import {
getOutlinedClasses,
type ColorName,
getDefaultClasses,
getSoftClasses,
colorPalette,
} from "@utils/colorUtils";
import { twMerge } from "tailwind-merge";
// PropTypes for documentation
export const propTypes = {
title: { type: "string", description: "The title of the alert" },
content: {
type: "string | JSX.Element",
description: "The content of the alert",
},
icon: {
type: "string | { name: string; class?: string }",
description: "The icon to display in the alert",
},
rightIcon: {
type: "string | { name: string; class?: string }",
description: "The icon to display on the right side of the alert",
},
iconPosition: {
type: ["left", "right"],
description: "The position of the icon",
default: "left",
},
class: {
type: "string",
description: "Additional CSS classes to apply to the alert",
},
dismissible: {
type: "boolean",
description: "Whether the alert is dismissible",
default: false,
},
variant: {
type: ["filled", "outline", "default"],
description: "The variant of the alert",
default: "default",
},
color: {
type: Object.keys(colorPalette),
description: "The color scheme for the alert",
default: "blue",
},
status: {
type: ["info", "success", "warning", "error"],
description: "The status of the alert",
},
borderAccent: {
type: ["top", "left", "right", "bottom", "none", "square"],
description: "The border accent position or shape",
default: "none",
},
size: {
type: ["sm", "md", "lg"],
description: "The size of the alert",
default: "md",
},
};
// Types and Interfaces
type AlertVariants = VariantProps<typeof alertStyles>;
interface Props extends HTMLAttributes<"div">, AlertVariants {
/**
* The title of the alert.
*/
title?: string;
/**
* The content of the alert.
*/
content?: string | JSX.Element;
/**
* The icon to display in the alert.
*/
icon?: string | { name: string; class?: string };
/**
* The icon to display on the right side of the alert.
*/
rightIcon?: string | { name: string; class?: string };
/**
* The position of the icon.
*/
iconPosition?: "left" | "right";
/**
* Additional CSS classes to apply to the alert.
*/
class?: string;
/**
* Whether the alert is dismissible.
*/
dismissible?: boolean;
/**
* The variant of the alert.
*/
variant?: "filled" | "outline" | "default";
/**
* The color scheme for the alert.
*/
color?: ColorName;
/**
* The status of the alert.
*/
status?: "info" | "success" | "warning" | "error";
/**
* The border accent position or shape.
*/
borderAccent?: "top" | "left" | "right" | "bottom" | "none" | "square";
/**
* The size of the alert.
*/
size?: "sm" | "md" | "lg";
}
// Component Logic
const {
title,
content,
icon,
rightIcon,
iconPosition = "left",
variant = "default",
status,
color = "blue",
dismissible = false,
borderAccent = "none",
class: className = "",
size = "md",
} = Astro.props as Props;
// Styles
const alertStyles = tv({
base: "flex items-start border",
variants: {
variant: {
filled: getDefaultClasses(color, { includeHover: false }),
outline: `${getOutlinedClasses(color, { includeHover: false })} border`,
default: `${getSoftClasses(color, { includeHover: false })} border`,
},
borderAccent: {
top: "rounded-b-lg border-t-4",
left: "rounded-r-lg border-l-4",
right: "rounded-l-lg border-r-4",
bottom: "rounded-t-lg border-b-4",
none: "rounded-lg",
square: "", // No rounded corners
},
size: {
sm: "p-3 text-xs",
md: "p-4 text-sm",
lg: "p-6 text-base",
},
status: {
info: getSoftClasses("blue"),
success: getSoftClasses("green"),
warning: getSoftClasses("yellow"),
error: getSoftClasses("red"),
},
},
defaultVariants: {
variant: "default",
size: "md",
borderAccent: "none",
status,
},
});
const getStatusIcon = (
status: Props["status"],
): { name: string; class: string } => {
const baseClass = "text-current";
switch (status) {
case "info":
return { name: "InformationCircleIcon", class: baseClass };
case "success":
return { name: "CheckCircleIcon", class: baseClass };
case "warning":
return { name: "ExclamationTriangleIcon", class: baseClass };
case "error":
return { name: "XCircleIcon", class: baseClass };
default:
return { name: "InformationCircleIcon", class: baseClass };
}
};
const finalIcon = icon
? typeof icon === "string"
? { name: icon, class: "" }
: icon
: status
? getStatusIcon(status)
: undefined;
const finalRightIcon = rightIcon
? typeof rightIcon === "string"
? { name: rightIcon, class: "" }
: rightIcon
: undefined;
---
<!-- Component Template -->
<div
class={twMerge(
alertStyles({ variant, borderAccent, size, status }),
className,
)}
role="alert"
aria-live="polite"
aria-labelledby={title ? "alert-title" : undefined}
>
{
finalIcon && iconPosition === "left" && (
<Icon
name={finalIcon.name}
class={twMerge(
`mr-3 ${
size === "sm" ? "w-4 h-4" : size === "lg" ? "w-6 h-6" : "w-5 h-5"
}`,
finalIcon.class,
)}
aria-hidden="true"
/>
)
}
<div class="flex-1">
{
title && (
<h3
id="alert-title"
class={twMerge(
"font-semibold -mt-1 mb-1 block",
size === "sm"
? "text-base"
: size === "md"
? "text-base"
: "text-lg",
)}
>
{title}
</h3>
)
}
{
content && (
<p>
<Fragment set:html={content} />
</p>
)
}
<slot />
</div>
{
(finalIcon && iconPosition === "right") || finalRightIcon ? (
<div class="flex items-center ml-3">
{finalIcon && iconPosition === "right" && (
<Icon
name={finalIcon.name}
class={twMerge(
`mt-0.5 ${
size === "sm"
? "w-4 h-4"
: size === "lg"
? "w-6 h-6"
: "w-5 h-5"
}`,
finalIcon.class,
)}
aria-hidden="true"
/>
)}
{finalRightIcon && (
<Icon
name={finalRightIcon.name}
class={twMerge(
`ml-2 mt-0.5 ${
size === "sm"
? "w-4 h-4"
: size === "lg"
? "w-6 h-6"
: "w-5 h-5"
}`,
finalRightIcon.class,
)}
aria-hidden="true"
/>
)}
</div>
) : null
}
{
dismissible && (
<button
type="button"
class={twMerge(
`ml-auto -mx-1.5 -my-1.5 rounded-lg focus:ring-2 p-1.5 inline-flex items-center justify-center transition-colors`,
`focus:ring-${color}-400 hover:bg-${color}-200 hover:text-${color}-900`,
)}
aria-label="Dismiss"
>
<Icon
name="XMarkIcon"
class={twMerge(
size === "sm" ? "w-4 h-4" : size === "lg" ? "w-6 h-6" : "w-5 h-5",
)}
aria-hidden="true"
/>
</button>
)
}
</div>
<div
class="flex items-start border bg-blue-500 dark:bg-blue-600 text-white dark:text-white border-blue-600 dark:border-blue-500 rounded-lg p-4 text-sm"
role="alert"
aria-live="polite">
<div class="flex-1">This is a filled alert.</div>
</div>
<div
class="flex items-start bg-transparent text-blue-500 dark:text-blue-400 border-blue-500 dark:border-blue-400 border rounded-lg p-4 text-sm mt-2"
role="alert"
aria-live="polite">
<div class="flex-1">This is an outlined alert.</div>
</div>
---
import { Alert } from '@/components/ui/alert';
---
<Alert variant="filled">This is a filled alert.</Alert>
<Alert variant="outline">This is an outlined alert.</Alert> ---
// Imports
import { type HTMLAttributes } from "astro/types";
import { tv, type VariantProps } from "@utils/custom-tv";
import Icon from "./Icon.astro";
import {
getOutlinedClasses,
type ColorName,
getDefaultClasses,
getSoftClasses,
colorPalette,
} from "@utils/colorUtils";
import { twMerge } from "tailwind-merge";
// PropTypes for documentation
export const propTypes = {
title: { type: "string", description: "The title of the alert" },
content: {
type: "string | JSX.Element",
description: "The content of the alert",
},
icon: {
type: "string | { name: string; class?: string }",
description: "The icon to display in the alert",
},
rightIcon: {
type: "string | { name: string; class?: string }",
description: "The icon to display on the right side of the alert",
},
iconPosition: {
type: ["left", "right"],
description: "The position of the icon",
default: "left",
},
class: {
type: "string",
description: "Additional CSS classes to apply to the alert",
},
dismissible: {
type: "boolean",
description: "Whether the alert is dismissible",
default: false,
},
variant: {
type: ["filled", "outline", "default"],
description: "The variant of the alert",
default: "default",
},
color: {
type: Object.keys(colorPalette),
description: "The color scheme for the alert",
default: "blue",
},
status: {
type: ["info", "success", "warning", "error"],
description: "The status of the alert",
},
borderAccent: {
type: ["top", "left", "right", "bottom", "none", "square"],
description: "The border accent position or shape",
default: "none",
},
size: {
type: ["sm", "md", "lg"],
description: "The size of the alert",
default: "md",
},
};
// Types and Interfaces
type AlertVariants = VariantProps<typeof alertStyles>;
interface Props extends HTMLAttributes<"div">, AlertVariants {
/**
* The title of the alert.
*/
title?: string;
/**
* The content of the alert.
*/
content?: string | JSX.Element;
/**
* The icon to display in the alert.
*/
icon?: string | { name: string; class?: string };
/**
* The icon to display on the right side of the alert.
*/
rightIcon?: string | { name: string; class?: string };
/**
* The position of the icon.
*/
iconPosition?: "left" | "right";
/**
* Additional CSS classes to apply to the alert.
*/
class?: string;
/**
* Whether the alert is dismissible.
*/
dismissible?: boolean;
/**
* The variant of the alert.
*/
variant?: "filled" | "outline" | "default";
/**
* The color scheme for the alert.
*/
color?: ColorName;
/**
* The status of the alert.
*/
status?: "info" | "success" | "warning" | "error";
/**
* The border accent position or shape.
*/
borderAccent?: "top" | "left" | "right" | "bottom" | "none" | "square";
/**
* The size of the alert.
*/
size?: "sm" | "md" | "lg";
}
// Component Logic
const {
title,
content,
icon,
rightIcon,
iconPosition = "left",
variant = "default",
status,
color = "blue",
dismissible = false,
borderAccent = "none",
class: className = "",
size = "md",
} = Astro.props as Props;
// Styles
const alertStyles = tv({
base: "flex items-start border",
variants: {
variant: {
filled: getDefaultClasses(color, { includeHover: false }),
outline: `${getOutlinedClasses(color, { includeHover: false })} border`,
default: `${getSoftClasses(color, { includeHover: false })} border`,
},
borderAccent: {
top: "rounded-b-lg border-t-4",
left: "rounded-r-lg border-l-4",
right: "rounded-l-lg border-r-4",
bottom: "rounded-t-lg border-b-4",
none: "rounded-lg",
square: "", // No rounded corners
},
size: {
sm: "p-3 text-xs",
md: "p-4 text-sm",
lg: "p-6 text-base",
},
status: {
info: getSoftClasses("blue"),
success: getSoftClasses("green"),
warning: getSoftClasses("yellow"),
error: getSoftClasses("red"),
},
},
defaultVariants: {
variant: "default",
size: "md",
borderAccent: "none",
status,
},
});
const getStatusIcon = (
status: Props["status"],
): { name: string; class: string } => {
const baseClass = "text-current";
switch (status) {
case "info":
return { name: "InformationCircleIcon", class: baseClass };
case "success":
return { name: "CheckCircleIcon", class: baseClass };
case "warning":
return { name: "ExclamationTriangleIcon", class: baseClass };
case "error":
return { name: "XCircleIcon", class: baseClass };
default:
return { name: "InformationCircleIcon", class: baseClass };
}
};
const finalIcon = icon
? typeof icon === "string"
? { name: icon, class: "" }
: icon
: status
? getStatusIcon(status)
: undefined;
const finalRightIcon = rightIcon
? typeof rightIcon === "string"
? { name: rightIcon, class: "" }
: rightIcon
: undefined;
---
<!-- Component Template -->
<div
class={twMerge(
alertStyles({ variant, borderAccent, size, status }),
className,
)}
role="alert"
aria-live="polite"
aria-labelledby={title ? "alert-title" : undefined}
>
{
finalIcon && iconPosition === "left" && (
<Icon
name={finalIcon.name}
class={twMerge(
`mr-3 ${
size === "sm" ? "w-4 h-4" : size === "lg" ? "w-6 h-6" : "w-5 h-5"
}`,
finalIcon.class,
)}
aria-hidden="true"
/>
)
}
<div class="flex-1">
{
title && (
<h3
id="alert-title"
class={twMerge(
"font-semibold -mt-1 mb-1 block",
size === "sm"
? "text-base"
: size === "md"
? "text-base"
: "text-lg",
)}
>
{title}
</h3>
)
}
{
content && (
<p>
<Fragment set:html={content} />
</p>
)
}
<slot />
</div>
{
(finalIcon && iconPosition === "right") || finalRightIcon ? (
<div class="flex items-center ml-3">
{finalIcon && iconPosition === "right" && (
<Icon
name={finalIcon.name}
class={twMerge(
`mt-0.5 ${
size === "sm"
? "w-4 h-4"
: size === "lg"
? "w-6 h-6"
: "w-5 h-5"
}`,
finalIcon.class,
)}
aria-hidden="true"
/>
)}
{finalRightIcon && (
<Icon
name={finalRightIcon.name}
class={twMerge(
`ml-2 mt-0.5 ${
size === "sm"
? "w-4 h-4"
: size === "lg"
? "w-6 h-6"
: "w-5 h-5"
}`,
finalRightIcon.class,
)}
aria-hidden="true"
/>
)}
</div>
) : null
}
{
dismissible && (
<button
type="button"
class={twMerge(
`ml-auto -mx-1.5 -my-1.5 rounded-lg focus:ring-2 p-1.5 inline-flex items-center justify-center transition-colors`,
`focus:ring-${color}-400 hover:bg-${color}-200 hover:text-${color}-900`,
)}
aria-label="Dismiss"
>
<Icon
name="XMarkIcon"
class={twMerge(
size === "sm" ? "w-4 h-4" : size === "lg" ? "w-6 h-6" : "w-5 h-5",
)}
aria-hidden="true"
/>
</button>
)
}
</div>
<div
class="flex items-start bg-blue-100 dark:bg-blue-800 text-blue-700 dark:text-blue-100 border-blue-200 dark:border-blue-600 border rounded-lg p-3 text-xs"
role="alert"
aria-live="polite">
<div class="flex-1">This is a small alert.</div>
</div>
<div
class="flex items-start bg-blue-100 dark:bg-blue-800 text-blue-700 dark:text-blue-100 border-blue-200 dark:border-blue-600 border rounded-lg p-4 text-sm mt-2"
role="alert"
aria-live="polite">
<div class="flex-1">This is a medium alert.</div>
</div>
<div
class="flex items-start bg-blue-100 dark:bg-blue-800 text-blue-700 dark:text-blue-100 border-blue-200 dark:border-blue-600 border rounded-lg p-6 text-base mt-2"
role="alert"
aria-live="polite">
<div class="flex-1">This is a large alert.</div>
</div>
---
import { Alert } from '@/components/ui/alert';
---
<Alert size="sm">This is a small alert.</Alert>
<Alert size="md">This is a medium alert.</Alert>
<Alert size="lg">This is a large alert.</Alert> ---
// Imports
import { type HTMLAttributes } from "astro/types";
import { tv, type VariantProps } from "@utils/custom-tv";
import Icon from "./Icon.astro";
import {
getOutlinedClasses,
type ColorName,
getDefaultClasses,
getSoftClasses,
colorPalette,
} from "@utils/colorUtils";
import { twMerge } from "tailwind-merge";
// PropTypes for documentation
export const propTypes = {
title: { type: "string", description: "The title of the alert" },
content: {
type: "string | JSX.Element",
description: "The content of the alert",
},
icon: {
type: "string | { name: string; class?: string }",
description: "The icon to display in the alert",
},
rightIcon: {
type: "string | { name: string; class?: string }",
description: "The icon to display on the right side of the alert",
},
iconPosition: {
type: ["left", "right"],
description: "The position of the icon",
default: "left",
},
class: {
type: "string",
description: "Additional CSS classes to apply to the alert",
},
dismissible: {
type: "boolean",
description: "Whether the alert is dismissible",
default: false,
},
variant: {
type: ["filled", "outline", "default"],
description: "The variant of the alert",
default: "default",
},
color: {
type: Object.keys(colorPalette),
description: "The color scheme for the alert",
default: "blue",
},
status: {
type: ["info", "success", "warning", "error"],
description: "The status of the alert",
},
borderAccent: {
type: ["top", "left", "right", "bottom", "none", "square"],
description: "The border accent position or shape",
default: "none",
},
size: {
type: ["sm", "md", "lg"],
description: "The size of the alert",
default: "md",
},
};
// Types and Interfaces
type AlertVariants = VariantProps<typeof alertStyles>;
interface Props extends HTMLAttributes<"div">, AlertVariants {
/**
* The title of the alert.
*/
title?: string;
/**
* The content of the alert.
*/
content?: string | JSX.Element;
/**
* The icon to display in the alert.
*/
icon?: string | { name: string; class?: string };
/**
* The icon to display on the right side of the alert.
*/
rightIcon?: string | { name: string; class?: string };
/**
* The position of the icon.
*/
iconPosition?: "left" | "right";
/**
* Additional CSS classes to apply to the alert.
*/
class?: string;
/**
* Whether the alert is dismissible.
*/
dismissible?: boolean;
/**
* The variant of the alert.
*/
variant?: "filled" | "outline" | "default";
/**
* The color scheme for the alert.
*/
color?: ColorName;
/**
* The status of the alert.
*/
status?: "info" | "success" | "warning" | "error";
/**
* The border accent position or shape.
*/
borderAccent?: "top" | "left" | "right" | "bottom" | "none" | "square";
/**
* The size of the alert.
*/
size?: "sm" | "md" | "lg";
}
// Component Logic
const {
title,
content,
icon,
rightIcon,
iconPosition = "left",
variant = "default",
status,
color = "blue",
dismissible = false,
borderAccent = "none",
class: className = "",
size = "md",
} = Astro.props as Props;
// Styles
const alertStyles = tv({
base: "flex items-start border",
variants: {
variant: {
filled: getDefaultClasses(color, { includeHover: false }),
outline: `${getOutlinedClasses(color, { includeHover: false })} border`,
default: `${getSoftClasses(color, { includeHover: false })} border`,
},
borderAccent: {
top: "rounded-b-lg border-t-4",
left: "rounded-r-lg border-l-4",
right: "rounded-l-lg border-r-4",
bottom: "rounded-t-lg border-b-4",
none: "rounded-lg",
square: "", // No rounded corners
},
size: {
sm: "p-3 text-xs",
md: "p-4 text-sm",
lg: "p-6 text-base",
},
status: {
info: getSoftClasses("blue"),
success: getSoftClasses("green"),
warning: getSoftClasses("yellow"),
error: getSoftClasses("red"),
},
},
defaultVariants: {
variant: "default",
size: "md",
borderAccent: "none",
status,
},
});
const getStatusIcon = (
status: Props["status"],
): { name: string; class: string } => {
const baseClass = "text-current";
switch (status) {
case "info":
return { name: "InformationCircleIcon", class: baseClass };
case "success":
return { name: "CheckCircleIcon", class: baseClass };
case "warning":
return { name: "ExclamationTriangleIcon", class: baseClass };
case "error":
return { name: "XCircleIcon", class: baseClass };
default:
return { name: "InformationCircleIcon", class: baseClass };
}
};
const finalIcon = icon
? typeof icon === "string"
? { name: icon, class: "" }
: icon
: status
? getStatusIcon(status)
: undefined;
const finalRightIcon = rightIcon
? typeof rightIcon === "string"
? { name: rightIcon, class: "" }
: rightIcon
: undefined;
---
<!-- Component Template -->
<div
class={twMerge(
alertStyles({ variant, borderAccent, size, status }),
className,
)}
role="alert"
aria-live="polite"
aria-labelledby={title ? "alert-title" : undefined}
>
{
finalIcon && iconPosition === "left" && (
<Icon
name={finalIcon.name}
class={twMerge(
`mr-3 ${
size === "sm" ? "w-4 h-4" : size === "lg" ? "w-6 h-6" : "w-5 h-5"
}`,
finalIcon.class,
)}
aria-hidden="true"
/>
)
}
<div class="flex-1">
{
title && (
<h3
id="alert-title"
class={twMerge(
"font-semibold -mt-1 mb-1 block",
size === "sm"
? "text-base"
: size === "md"
? "text-base"
: "text-lg",
)}
>
{title}
</h3>
)
}
{
content && (
<p>
<Fragment set:html={content} />
</p>
)
}
<slot />
</div>
{
(finalIcon && iconPosition === "right") || finalRightIcon ? (
<div class="flex items-center ml-3">
{finalIcon && iconPosition === "right" && (
<Icon
name={finalIcon.name}
class={twMerge(
`mt-0.5 ${
size === "sm"
? "w-4 h-4"
: size === "lg"
? "w-6 h-6"
: "w-5 h-5"
}`,
finalIcon.class,
)}
aria-hidden="true"
/>
)}
{finalRightIcon && (
<Icon
name={finalRightIcon.name}
class={twMerge(
`ml-2 mt-0.5 ${
size === "sm"
? "w-4 h-4"
: size === "lg"
? "w-6 h-6"
: "w-5 h-5"
}`,
finalRightIcon.class,
)}
aria-hidden="true"
/>
)}
</div>
) : null
}
{
dismissible && (
<button
type="button"
class={twMerge(
`ml-auto -mx-1.5 -my-1.5 rounded-lg focus:ring-2 p-1.5 inline-flex items-center justify-center transition-colors`,
`focus:ring-${color}-400 hover:bg-${color}-200 hover:text-${color}-900`,
)}
aria-label="Dismiss"
>
<Icon
name="XMarkIcon"
class={twMerge(
size === "sm" ? "w-4 h-4" : size === "lg" ? "w-6 h-6" : "w-5 h-5",
)}
aria-hidden="true"
/>
</button>
)
}
</div>
<div
class="flex items-start bg-blue-100 dark:bg-blue-800 text-blue-700 dark:text-blue-100 border-blue-200 dark:border-blue-600 border rounded-b-lg border-t-4 p-4 text-sm"
role="alert"
aria-live="polite">
<div class="flex-1">Top border accent</div>
</div>
<div
class="flex items-start bg-blue-100 dark:bg-blue-800 text-blue-700 dark:text-blue-100 border-blue-200 dark:border-blue-600 border rounded-r-lg border-l-4 p-4 text-sm mt-2"
role="alert"
aria-live="polite">
<div class="flex-1">Left border accent</div>
</div>
<div
class="flex items-start bg-blue-100 dark:bg-blue-800 text-blue-700 dark:text-blue-100 border-blue-200 dark:border-blue-600 border rounded-l-lg border-r-4 p-4 text-sm mt-2"
role="alert"
aria-live="polite">
<div class="flex-1">Right border accent</div>
</div>
<div
class="flex items-start bg-blue-100 dark:bg-blue-800 text-blue-700 dark:text-blue-100 border-blue-200 dark:border-blue-600 border rounded-t-lg border-b-4 p-4 text-sm mt-2"
role="alert"
aria-live="polite">
<div class="flex-1">Bottom border accent</div>
</div>
<div
class="flex items-start bg-blue-100 dark:bg-blue-800 text-blue-700 dark:text-blue-100 border-blue-200 dark:border-blue-600 border p-4 text-sm mt-2"
role="alert"
aria-live="polite">
<div class="flex-1">Square border accent</div>
</div>
---
import { Alert } from '@/components/ui/alert';
---
<Alert borderAccent="top">Top border accent</Alert>
<Alert borderAccent="left">Left border accent</Alert>
<Alert borderAccent="right">Right border accent</Alert>
<Alert borderAccent="bottom">Bottom border accent</Alert>
<Alert borderAccent="square">Square border accent</Alert> ---
// Imports
import { type HTMLAttributes } from "astro/types";
import { tv, type VariantProps } from "@utils/custom-tv";
import Icon from "./Icon.astro";
import {
getOutlinedClasses,
type ColorName,
getDefaultClasses,
getSoftClasses,
colorPalette,
} from "@utils/colorUtils";
import { twMerge } from "tailwind-merge";
// PropTypes for documentation
export const propTypes = {
title: { type: "string", description: "The title of the alert" },
content: {
type: "string | JSX.Element",
description: "The content of the alert",
},
icon: {
type: "string | { name: string; class?: string }",
description: "The icon to display in the alert",
},
rightIcon: {
type: "string | { name: string; class?: string }",
description: "The icon to display on the right side of the alert",
},
iconPosition: {
type: ["left", "right"],
description: "The position of the icon",
default: "left",
},
class: {
type: "string",
description: "Additional CSS classes to apply to the alert",
},
dismissible: {
type: "boolean",
description: "Whether the alert is dismissible",
default: false,
},
variant: {
type: ["filled", "outline", "default"],
description: "The variant of the alert",
default: "default",
},
color: {
type: Object.keys(colorPalette),
description: "The color scheme for the alert",
default: "blue",
},
status: {
type: ["info", "success", "warning", "error"],
description: "The status of the alert",
},
borderAccent: {
type: ["top", "left", "right", "bottom", "none", "square"],
description: "The border accent position or shape",
default: "none",
},
size: {
type: ["sm", "md", "lg"],
description: "The size of the alert",
default: "md",
},
};
// Types and Interfaces
type AlertVariants = VariantProps<typeof alertStyles>;
interface Props extends HTMLAttributes<"div">, AlertVariants {
/**
* The title of the alert.
*/
title?: string;
/**
* The content of the alert.
*/
content?: string | JSX.Element;
/**
* The icon to display in the alert.
*/
icon?: string | { name: string; class?: string };
/**
* The icon to display on the right side of the alert.
*/
rightIcon?: string | { name: string; class?: string };
/**
* The position of the icon.
*/
iconPosition?: "left" | "right";
/**
* Additional CSS classes to apply to the alert.
*/
class?: string;
/**
* Whether the alert is dismissible.
*/
dismissible?: boolean;
/**
* The variant of the alert.
*/
variant?: "filled" | "outline" | "default";
/**
* The color scheme for the alert.
*/
color?: ColorName;
/**
* The status of the alert.
*/
status?: "info" | "success" | "warning" | "error";
/**
* The border accent position or shape.
*/
borderAccent?: "top" | "left" | "right" | "bottom" | "none" | "square";
/**
* The size of the alert.
*/
size?: "sm" | "md" | "lg";
}
// Component Logic
const {
title,
content,
icon,
rightIcon,
iconPosition = "left",
variant = "default",
status,
color = "blue",
dismissible = false,
borderAccent = "none",
class: className = "",
size = "md",
} = Astro.props as Props;
// Styles
const alertStyles = tv({
base: "flex items-start border",
variants: {
variant: {
filled: getDefaultClasses(color, { includeHover: false }),
outline: `${getOutlinedClasses(color, { includeHover: false })} border`,
default: `${getSoftClasses(color, { includeHover: false })} border`,
},
borderAccent: {
top: "rounded-b-lg border-t-4",
left: "rounded-r-lg border-l-4",
right: "rounded-l-lg border-r-4",
bottom: "rounded-t-lg border-b-4",
none: "rounded-lg",
square: "", // No rounded corners
},
size: {
sm: "p-3 text-xs",
md: "p-4 text-sm",
lg: "p-6 text-base",
},
status: {
info: getSoftClasses("blue"),
success: getSoftClasses("green"),
warning: getSoftClasses("yellow"),
error: getSoftClasses("red"),
},
},
defaultVariants: {
variant: "default",
size: "md",
borderAccent: "none",
status,
},
});
const getStatusIcon = (
status: Props["status"],
): { name: string; class: string } => {
const baseClass = "text-current";
switch (status) {
case "info":
return { name: "InformationCircleIcon", class: baseClass };
case "success":
return { name: "CheckCircleIcon", class: baseClass };
case "warning":
return { name: "ExclamationTriangleIcon", class: baseClass };
case "error":
return { name: "XCircleIcon", class: baseClass };
default:
return { name: "InformationCircleIcon", class: baseClass };
}
};
const finalIcon = icon
? typeof icon === "string"
? { name: icon, class: "" }
: icon
: status
? getStatusIcon(status)
: undefined;
const finalRightIcon = rightIcon
? typeof rightIcon === "string"
? { name: rightIcon, class: "" }
: rightIcon
: undefined;
---
<!-- Component Template -->
<div
class={twMerge(
alertStyles({ variant, borderAccent, size, status }),
className,
)}
role="alert"
aria-live="polite"
aria-labelledby={title ? "alert-title" : undefined}
>
{
finalIcon && iconPosition === "left" && (
<Icon
name={finalIcon.name}
class={twMerge(
`mr-3 ${
size === "sm" ? "w-4 h-4" : size === "lg" ? "w-6 h-6" : "w-5 h-5"
}`,
finalIcon.class,
)}
aria-hidden="true"
/>
)
}
<div class="flex-1">
{
title && (
<h3
id="alert-title"
class={twMerge(
"font-semibold -mt-1 mb-1 block",
size === "sm"
? "text-base"
: size === "md"
? "text-base"
: "text-lg",
)}
>
{title}
</h3>
)
}
{
content && (
<p>
<Fragment set:html={content} />
</p>
)
}
<slot />
</div>
{
(finalIcon && iconPosition === "right") || finalRightIcon ? (
<div class="flex items-center ml-3">
{finalIcon && iconPosition === "right" && (
<Icon
name={finalIcon.name}
class={twMerge(
`mt-0.5 ${
size === "sm"
? "w-4 h-4"
: size === "lg"
? "w-6 h-6"
: "w-5 h-5"
}`,
finalIcon.class,
)}
aria-hidden="true"
/>
)}
{finalRightIcon && (
<Icon
name={finalRightIcon.name}
class={twMerge(
`ml-2 mt-0.5 ${
size === "sm"
? "w-4 h-4"
: size === "lg"
? "w-6 h-6"
: "w-5 h-5"
}`,
finalRightIcon.class,
)}
aria-hidden="true"
/>
)}
</div>
) : null
}
{
dismissible && (
<button
type="button"
class={twMerge(
`ml-auto -mx-1.5 -my-1.5 rounded-lg focus:ring-2 p-1.5 inline-flex items-center justify-center transition-colors`,
`focus:ring-${color}-400 hover:bg-${color}-200 hover:text-${color}-900`,
)}
aria-label="Dismiss"
>
<Icon
name="XMarkIcon"
class={twMerge(
size === "sm" ? "w-4 h-4" : size === "lg" ? "w-6 h-6" : "w-5 h-5",
)}
aria-hidden="true"
/>
</button>
)
}
</div>
<div
class="flex items-start bg-blue-100 dark:bg-blue-800 text-blue-700 dark:text-blue-100 border-blue-200 dark:border-blue-600 border rounded-lg p-4 text-sm"
role="alert"
aria-live="polite">
<span class="inline-block size-5 mr-3 w-5 h-5">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
aria-hidden="true"
data-slot="icon">
<path
fill-rule="evenodd"
d="M2.25 12c0-5.385 4.365-9.75 9.75-9.75s9.75 4.365 9.75 9.75-4.365 9.75-9.75 9.75S2.25 17.385 2.25 12Zm8.706-1.442c1.146-.573 2.437.463 2.126 1.706l-.709 2.836.042-.02a.75.75 0 0 1 .67 1.34l-.04.022c-1.147.573-2.438-.463-2.127-1.706l.71-2.836-.042.02a.75.75 0 1 1-.671-1.34l.041-.022ZM12 9a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5Z"
clip-rule="evenodd"></path>
</svg>
</span>
<div class="flex-1">Left icon position</div>
</div>
<div
class="flex items-start bg-blue-100 dark:bg-blue-800 text-blue-700 dark:text-blue-100 border-blue-200 dark:border-blue-600 border rounded-lg p-4 text-sm mt-2"
role="alert"
aria-live="polite">
<div class="flex-1">Right icon position</div>
<div class="flex items-center ml-3">
<span class="inline-block size-5 mt-0.5 w-5 h-5">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
aria-hidden="true"
data-slot="icon">
<path
fill-rule="evenodd"
d="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 12Zm8.706-1.442c1.146-.573 2.437.463 2.126 1.706l-.709 2.836.042-.02a.75.75 0 0 1 .67 1.34l-.04.022c-1.147.573-2.438-.463-2.127-1.706l.71-2.836-.042.02a.75.75 0 1 1-.671-1.34l.041-.022ZM12 9a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5Z"
clip-rule="evenodd"></path>
</svg>
</span>
</div>
</div>
---
import { Alert } from '@/components/ui/alert';
---
<Alert icon="InformationCircleIcon" iconPosition="left">Left icon position</Alert>
<Alert icon="InformationCircleIcon" iconPosition="right">Right icon position</Alert> ---
// Imports
import { type HTMLAttributes } from "astro/types";
import { tv, type VariantProps } from "@utils/custom-tv";
import Icon from "./Icon.astro";
import {
getOutlinedClasses,
type ColorName,
getDefaultClasses,
getSoftClasses,
colorPalette,
} from "@utils/colorUtils";
import { twMerge } from "tailwind-merge";
// PropTypes for documentation
export const propTypes = {
title: { type: "string", description: "The title of the alert" },
content: {
type: "string | JSX.Element",
description: "The content of the alert",
},
icon: {
type: "string | { name: string; class?: string }",
description: "The icon to display in the alert",
},
rightIcon: {
type: "string | { name: string; class?: string }",
description: "The icon to display on the right side of the alert",
},
iconPosition: {
type: ["left", "right"],
description: "The position of the icon",
default: "left",
},
class: {
type: "string",
description: "Additional CSS classes to apply to the alert",
},
dismissible: {
type: "boolean",
description: "Whether the alert is dismissible",
default: false,
},
variant: {
type: ["filled", "outline", "default"],
description: "The variant of the alert",
default: "default",
},
color: {
type: Object.keys(colorPalette),
description: "The color scheme for the alert",
default: "blue",
},
status: {
type: ["info", "success", "warning", "error"],
description: "The status of the alert",
},
borderAccent: {
type: ["top", "left", "right", "bottom", "none", "square"],
description: "The border accent position or shape",
default: "none",
},
size: {
type: ["sm", "md", "lg"],
description: "The size of the alert",
default: "md",
},
};
// Types and Interfaces
type AlertVariants = VariantProps<typeof alertStyles>;
interface Props extends HTMLAttributes<"div">, AlertVariants {
/**
* The title of the alert.
*/
title?: string;
/**
* The content of the alert.
*/
content?: string | JSX.Element;
/**
* The icon to display in the alert.
*/
icon?: string | { name: string; class?: string };
/**
* The icon to display on the right side of the alert.
*/
rightIcon?: string | { name: string; class?: string };
/**
* The position of the icon.
*/
iconPosition?: "left" | "right";
/**
* Additional CSS classes to apply to the alert.
*/
class?: string;
/**
* Whether the alert is dismissible.
*/
dismissible?: boolean;
/**
* The variant of the alert.
*/
variant?: "filled" | "outline" | "default";
/**
* The color scheme for the alert.
*/
color?: ColorName;
/**
* The status of the alert.
*/
status?: "info" | "success" | "warning" | "error";
/**
* The border accent position or shape.
*/
borderAccent?: "top" | "left" | "right" | "bottom" | "none" | "square";
/**
* The size of the alert.
*/
size?: "sm" | "md" | "lg";
}
// Component Logic
const {
title,
content,
icon,
rightIcon,
iconPosition = "left",
variant = "default",
status,
color = "blue",
dismissible = false,
borderAccent = "none",
class: className = "",
size = "md",
} = Astro.props as Props;
// Styles
const alertStyles = tv({
base: "flex items-start border",
variants: {
variant: {
filled: getDefaultClasses(color, { includeHover: false }),
outline: `${getOutlinedClasses(color, { includeHover: false })} border`,
default: `${getSoftClasses(color, { includeHover: false })} border`,
},
borderAccent: {
top: "rounded-b-lg border-t-4",
left: "rounded-r-lg border-l-4",
right: "rounded-l-lg border-r-4",
bottom: "rounded-t-lg border-b-4",
none: "rounded-lg",
square: "", // No rounded corners
},
size: {
sm: "p-3 text-xs",
md: "p-4 text-sm",
lg: "p-6 text-base",
},
status: {
info: getSoftClasses("blue"),
success: getSoftClasses("green"),
warning: getSoftClasses("yellow"),
error: getSoftClasses("red"),
},
},
defaultVariants: {
variant: "default",
size: "md",
borderAccent: "none",
status,
},
});
const getStatusIcon = (
status: Props["status"],
): { name: string; class: string } => {
const baseClass = "text-current";
switch (status) {
case "info":
return { name: "InformationCircleIcon", class: baseClass };
case "success":
return { name: "CheckCircleIcon", class: baseClass };
case "warning":
return { name: "ExclamationTriangleIcon", class: baseClass };
case "error":
return { name: "XCircleIcon", class: baseClass };
default:
return { name: "InformationCircleIcon", class: baseClass };
}
};
const finalIcon = icon
? typeof icon === "string"
? { name: icon, class: "" }
: icon
: status
? getStatusIcon(status)
: undefined;
const finalRightIcon = rightIcon
? typeof rightIcon === "string"
? { name: rightIcon, class: "" }
: rightIcon
: undefined;
---
<!-- Component Template -->
<div
class={twMerge(
alertStyles({ variant, borderAccent, size, status }),
className,
)}
role="alert"
aria-live="polite"
aria-labelledby={title ? "alert-title" : undefined}
>
{
finalIcon && iconPosition === "left" && (
<Icon
name={finalIcon.name}
class={twMerge(
`mr-3 ${
size === "sm" ? "w-4 h-4" : size === "lg" ? "w-6 h-6" : "w-5 h-5"
}`,
finalIcon.class,
)}
aria-hidden="true"
/>
)
}
<div class="flex-1">
{
title && (
<h3
id="alert-title"
class={twMerge(
"font-semibold -mt-1 mb-1 block",
size === "sm"
? "text-base"
: size === "md"
? "text-base"
: "text-lg",
)}
>
{title}
</h3>
)
}
{
content && (
<p>
<Fragment set:html={content} />
</p>
)
}
<slot />
</div>
{
(finalIcon && iconPosition === "right") || finalRightIcon ? (
<div class="flex items-center ml-3">
{finalIcon && iconPosition === "right" && (
<Icon
name={finalIcon.name}
class={twMerge(
`mt-0.5 ${
size === "sm"
? "w-4 h-4"
: size === "lg"
? "w-6 h-6"
: "w-5 h-5"
}`,
finalIcon.class,
)}
aria-hidden="true"
/>
)}
{finalRightIcon && (
<Icon
name={finalRightIcon.name}
class={twMerge(
`ml-2 mt-0.5 ${
size === "sm"
? "w-4 h-4"
: size === "lg"
? "w-6 h-6"
: "w-5 h-5"
}`,
finalRightIcon.class,
)}
aria-hidden="true"
/>
)}
</div>
) : null
}
{
dismissible && (
<button
type="button"
class={twMerge(
`ml-auto -mx-1.5 -my-1.5 rounded-lg focus:ring-2 p-1.5 inline-flex items-center justify-center transition-colors`,
`focus:ring-${color}-400 hover:bg-${color}-200 hover:text-${color}-900`,
)}
aria-label="Dismiss"
>
<Icon
name="XMarkIcon"
class={twMerge(
size === "sm" ? "w-4 h-4" : size === "lg" ? "w-6 h-6" : "w-5 h-5",
)}
aria-hidden="true"
/>
</button>
)
}
</div>
Alert Statuses
Alerts with different statuses, which automatically set the color and icon.
<div
class="flex items-start border rounded-lg p-4 text-sm 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"
role="alert"
aria-live="polite">
<span class="inline-block size-5 mr-3 w-5 h-5 text-current">
<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 12Zm8.706-1.442c1.146-.573 2.437.463 2.126 1.706l-.709 2.836.042-.02a.75.75 0 0 1 .67 1.34l-.04.022c-1.147.573-2.438-.463-2.127-1.706l.71-2.836-.042.02a.75.75 0 1 1-.671-1.34l.041-.022ZM12 9a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5Z"
clip-rule="evenodd"></path>
</svg>
</span>
<div class="flex-1">Information alert</div>
</div>
<div
class="flex items-start border rounded-lg p-4 text-sm bg-green-100 dark:bg-green-800 hover:bg-green-200 dark:hover:bg-green-700 text-green-700 dark:text-green-100 border-green-200 dark:border-green-600 mt-2"
role="alert"
aria-live="polite">
<span class="inline-block size-5 mr-3 w-5 h-5 text-current">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
aria-hidden="true"
data-slot="icon">
<path
fill-rule="evenodd"
d="M2.25 12c0-5.385 4.365-9.75 9.75-9.75s9.75 4.365 9.75 9.75-4.365 9.75-9.75 9.75S2.25 17.385 2.25 12Zm13.36-1.814a.75.75 0 1 0-1.22-.872l-3.236 4.53L9.53 12.22a.75.75 0 0 0-1.06 1.06l2.25 2.25a.75.75 0 0 0 1.14-.094l3.75-5.25Z"
clip-rule="evenodd"></path>
</svg>
</span>
<div class="flex-1">Success alert</div>
</div>
<div
class="flex items-start border rounded-lg p-4 text-sm 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 mt-2"
role="alert"
aria-live="polite">
<span class="inline-block size-5 mr-3 w-5 h-5 text-current">
<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="M9.401 3.003c1.155-2 4.043-2 5.197 0l7.355 12.748c1.154 2-.29 4.5-2.599 4.5H4.645c-2.309 0-3.752-2.5-2.598-4.5L9.4 3.003ZM12 8.25a.75.75 0 0 1 .75.75v3.75a.75.75 0 0 1-1.5 0V9a.75.75 0 0 1 .75-.75Zm0 8.25a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5Z"
clip-rule="evenodd"></path>
</svg>
</span>
<div class="flex-1">Warning alert</div>
</div>
<div
class="flex items-start border rounded-lg p-4 text-sm bg-red-100 dark:bg-red-800 hover:bg-red-200 dark:hover:bg-red-700 text-red-700 dark:text-red-100 border-red-200 dark:border-red-600 mt-2"
role="alert"
aria-live="polite">
<span class="inline-block size-5 mr-3 w-5 h-5 text-current">
<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.25c-5.385 0-9.75 4.365-9.75 9.75s4.365 9.75 9.75 9.75 9.75-4.365 9.75-9.75S17.385 2.25 12 2.25Zm-1.72 6.97a.75.75 0 1 0-1.06 1.06L10.94 12l-1.72 1.72a.75.75 0 1 0 1.06 1.06L12 13.06l1.72 1.72a.75.75 0 1 0 1.06-1.06L13.06 12l1.72-1.72a.75.75 0 1 0-1.06-1.06L12 10.94l-1.72-1.72Z"
clip-rule="evenodd"></path>
</svg>
</span>
<div class="flex-1">Error alert</div>
</div>
---
import { Alert } from '@/components/ui/alert';
---
<Alert status="info">Information alert</Alert>
<Alert status="success">Success alert</Alert>
<Alert status="warning">Warning alert</Alert>
<Alert status="error">Error alert</Alert> ---
// Imports
import { type HTMLAttributes } from "astro/types";
import { tv, type VariantProps } from "@utils/custom-tv";
import Icon from "./Icon.astro";
import {
getOutlinedClasses,
type ColorName,
getDefaultClasses,
getSoftClasses,
colorPalette,
} from "@utils/colorUtils";
import { twMerge } from "tailwind-merge";
// PropTypes for documentation
export const propTypes = {
title: { type: "string", description: "The title of the alert" },
content: {
type: "string | JSX.Element",
description: "The content of the alert",
},
icon: {
type: "string | { name: string; class?: string }",
description: "The icon to display in the alert",
},
rightIcon: {
type: "string | { name: string; class?: string }",
description: "The icon to display on the right side of the alert",
},
iconPosition: {
type: ["left", "right"],
description: "The position of the icon",
default: "left",
},
class: {
type: "string",
description: "Additional CSS classes to apply to the alert",
},
dismissible: {
type: "boolean",
description: "Whether the alert is dismissible",
default: false,
},
variant: {
type: ["filled", "outline", "default"],
description: "The variant of the alert",
default: "default",
},
color: {
type: Object.keys(colorPalette),
description: "The color scheme for the alert",
default: "blue",
},
status: {
type: ["info", "success", "warning", "error"],
description: "The status of the alert",
},
borderAccent: {
type: ["top", "left", "right", "bottom", "none", "square"],
description: "The border accent position or shape",
default: "none",
},
size: {
type: ["sm", "md", "lg"],
description: "The size of the alert",
default: "md",
},
};
// Types and Interfaces
type AlertVariants = VariantProps<typeof alertStyles>;
interface Props extends HTMLAttributes<"div">, AlertVariants {
/**
* The title of the alert.
*/
title?: string;
/**
* The content of the alert.
*/
content?: string | JSX.Element;
/**
* The icon to display in the alert.
*/
icon?: string | { name: string; class?: string };
/**
* The icon to display on the right side of the alert.
*/
rightIcon?: string | { name: string; class?: string };
/**
* The position of the icon.
*/
iconPosition?: "left" | "right";
/**
* Additional CSS classes to apply to the alert.
*/
class?: string;
/**
* Whether the alert is dismissible.
*/
dismissible?: boolean;
/**
* The variant of the alert.
*/
variant?: "filled" | "outline" | "default";
/**
* The color scheme for the alert.
*/
color?: ColorName;
/**
* The status of the alert.
*/
status?: "info" | "success" | "warning" | "error";
/**
* The border accent position or shape.
*/
borderAccent?: "top" | "left" | "right" | "bottom" | "none" | "square";
/**
* The size of the alert.
*/
size?: "sm" | "md" | "lg";
}
// Component Logic
const {
title,
content,
icon,
rightIcon,
iconPosition = "left",
variant = "default",
status,
color = "blue",
dismissible = false,
borderAccent = "none",
class: className = "",
size = "md",
} = Astro.props as Props;
// Styles
const alertStyles = tv({
base: "flex items-start border",
variants: {
variant: {
filled: getDefaultClasses(color, { includeHover: false }),
outline: `${getOutlinedClasses(color, { includeHover: false })} border`,
default: `${getSoftClasses(color, { includeHover: false })} border`,
},
borderAccent: {
top: "rounded-b-lg border-t-4",
left: "rounded-r-lg border-l-4",
right: "rounded-l-lg border-r-4",
bottom: "rounded-t-lg border-b-4",
none: "rounded-lg",
square: "", // No rounded corners
},
size: {
sm: "p-3 text-xs",
md: "p-4 text-sm",
lg: "p-6 text-base",
},
status: {
info: getSoftClasses("blue"),
success: getSoftClasses("green"),
warning: getSoftClasses("yellow"),
error: getSoftClasses("red"),
},
},
defaultVariants: {
variant: "default",
size: "md",
borderAccent: "none",
status,
},
});
const getStatusIcon = (
status: Props["status"],
): { name: string; class: string } => {
const baseClass = "text-current";
switch (status) {
case "info":
return { name: "InformationCircleIcon", class: baseClass };
case "success":
return { name: "CheckCircleIcon", class: baseClass };
case "warning":
return { name: "ExclamationTriangleIcon", class: baseClass };
case "error":
return { name: "XCircleIcon", class: baseClass };
default:
return { name: "InformationCircleIcon", class: baseClass };
}
};
const finalIcon = icon
? typeof icon === "string"
? { name: icon, class: "" }
: icon
: status
? getStatusIcon(status)
: undefined;
const finalRightIcon = rightIcon
? typeof rightIcon === "string"
? { name: rightIcon, class: "" }
: rightIcon
: undefined;
---
<!-- Component Template -->
<div
class={twMerge(
alertStyles({ variant, borderAccent, size, status }),
className,
)}
role="alert"
aria-live="polite"
aria-labelledby={title ? "alert-title" : undefined}
>
{
finalIcon && iconPosition === "left" && (
<Icon
name={finalIcon.name}
class={twMerge(
`mr-3 ${
size === "sm" ? "w-4 h-4" : size === "lg" ? "w-6 h-6" : "w-5 h-5"
}`,
finalIcon.class,
)}
aria-hidden="true"
/>
)
}
<div class="flex-1">
{
title && (
<h3
id="alert-title"
class={twMerge(
"font-semibold -mt-1 mb-1 block",
size === "sm"
? "text-base"
: size === "md"
? "text-base"
: "text-lg",
)}
>
{title}
</h3>
)
}
{
content && (
<p>
<Fragment set:html={content} />
</p>
)
}
<slot />
</div>
{
(finalIcon && iconPosition === "right") || finalRightIcon ? (
<div class="flex items-center ml-3">
{finalIcon && iconPosition === "right" && (
<Icon
name={finalIcon.name}
class={twMerge(
`mt-0.5 ${
size === "sm"
? "w-4 h-4"
: size === "lg"
? "w-6 h-6"
: "w-5 h-5"
}`,
finalIcon.class,
)}
aria-hidden="true"
/>
)}
{finalRightIcon && (
<Icon
name={finalRightIcon.name}
class={twMerge(
`ml-2 mt-0.5 ${
size === "sm"
? "w-4 h-4"
: size === "lg"
? "w-6 h-6"
: "w-5 h-5"
}`,
finalRightIcon.class,
)}
aria-hidden="true"
/>
)}
</div>
) : null
}
{
dismissible && (
<button
type="button"
class={twMerge(
`ml-auto -mx-1.5 -my-1.5 rounded-lg focus:ring-2 p-1.5 inline-flex items-center justify-center transition-colors`,
`focus:ring-${color}-400 hover:bg-${color}-200 hover:text-${color}-900`,
)}
aria-label="Dismiss"
>
<Icon
name="XMarkIcon"
class={twMerge(
size === "sm" ? "w-4 h-4" : size === "lg" ? "w-6 h-6" : "w-5 h-5",
)}
aria-hidden="true"
/>
</button>
)
}
</div>
<div
class="flex items-start bg-blue-100 dark:bg-blue-800 text-blue-700 dark:text-blue-100 border-blue-200 dark:border-blue-600 border rounded-lg p-4 text-sm"
role="alert"
aria-live="polite">
<div class="flex-1"><p>This is a dismissible alert. Click the X to close it.</p></div>
<button
type="button"
class="-mx-1.5 -my-1.5 rounded-lg focus:ring-2 p-1.5 inline-flex items-center justify-center transition-colors focus:ring-blue-400 hover:bg-blue-200 hover:text-blue-900"
aria-label="Dismiss">
<span class="inline-block size-5 w-5 h-5">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
aria-hidden="true"
data-slot="icon">
<path
fill-rule="evenodd"
d="M5.47 5.47a.75.75 0 0 1 1.06 0L12 10.94l5.47-5.47a.75.75 0 1 1 1.06 1.06L13.06 12l5.47 5.47a.75.75 0 1 1-1.06 1.06L12 13.06l-5.47 5.47a.75.75 0 0 1-1.06-1.06L10.94 12 5.47 6.53a.75.75 0 0 1 0-1.06Z"
clip-rule="evenodd"></path>
</svg>
</span>
</button>
</div>
---
import { Alert } from '@/components/ui/alert';
---
<Alert dismissible>
This is a dismissible alert. Click the X to close it.
</Alert> ---
// Imports
import { type HTMLAttributes } from "astro/types";
import { tv, type VariantProps } from "@utils/custom-tv";
import Icon from "./Icon.astro";
import {
getOutlinedClasses,
type ColorName,
getDefaultClasses,
getSoftClasses,
colorPalette,
} from "@utils/colorUtils";
import { twMerge } from "tailwind-merge";
// PropTypes for documentation
export const propTypes = {
title: { type: "string", description: "The title of the alert" },
content: {
type: "string | JSX.Element",
description: "The content of the alert",
},
icon: {
type: "string | { name: string; class?: string }",
description: "The icon to display in the alert",
},
rightIcon: {
type: "string | { name: string; class?: string }",
description: "The icon to display on the right side of the alert",
},
iconPosition: {
type: ["left", "right"],
description: "The position of the icon",
default: "left",
},
class: {
type: "string",
description: "Additional CSS classes to apply to the alert",
},
dismissible: {
type: "boolean",
description: "Whether the alert is dismissible",
default: false,
},
variant: {
type: ["filled", "outline", "default"],
description: "The variant of the alert",
default: "default",
},
color: {
type: Object.keys(colorPalette),
description: "The color scheme for the alert",
default: "blue",
},
status: {
type: ["info", "success", "warning", "error"],
description: "The status of the alert",
},
borderAccent: {
type: ["top", "left", "right", "bottom", "none", "square"],
description: "The border accent position or shape",
default: "none",
},
size: {
type: ["sm", "md", "lg"],
description: "The size of the alert",
default: "md",
},
};
// Types and Interfaces
type AlertVariants = VariantProps<typeof alertStyles>;
interface Props extends HTMLAttributes<"div">, AlertVariants {
/**
* The title of the alert.
*/
title?: string;
/**
* The content of the alert.
*/
content?: string | JSX.Element;
/**
* The icon to display in the alert.
*/
icon?: string | { name: string; class?: string };
/**
* The icon to display on the right side of the alert.
*/
rightIcon?: string | { name: string; class?: string };
/**
* The position of the icon.
*/
iconPosition?: "left" | "right";
/**
* Additional CSS classes to apply to the alert.
*/
class?: string;
/**
* Whether the alert is dismissible.
*/
dismissible?: boolean;
/**
* The variant of the alert.
*/
variant?: "filled" | "outline" | "default";
/**
* The color scheme for the alert.
*/
color?: ColorName;
/**
* The status of the alert.
*/
status?: "info" | "success" | "warning" | "error";
/**
* The border accent position or shape.
*/
borderAccent?: "top" | "left" | "right" | "bottom" | "none" | "square";
/**
* The size of the alert.
*/
size?: "sm" | "md" | "lg";
}
// Component Logic
const {
title,
content,
icon,
rightIcon,
iconPosition = "left",
variant = "default",
status,
color = "blue",
dismissible = false,
borderAccent = "none",
class: className = "",
size = "md",
} = Astro.props as Props;
// Styles
const alertStyles = tv({
base: "flex items-start border",
variants: {
variant: {
filled: getDefaultClasses(color, { includeHover: false }),
outline: `${getOutlinedClasses(color, { includeHover: false })} border`,
default: `${getSoftClasses(color, { includeHover: false })} border`,
},
borderAccent: {
top: "rounded-b-lg border-t-4",
left: "rounded-r-lg border-l-4",
right: "rounded-l-lg border-r-4",
bottom: "rounded-t-lg border-b-4",
none: "rounded-lg",
square: "", // No rounded corners
},
size: {
sm: "p-3 text-xs",
md: "p-4 text-sm",
lg: "p-6 text-base",
},
status: {
info: getSoftClasses("blue"),
success: getSoftClasses("green"),
warning: getSoftClasses("yellow"),
error: getSoftClasses("red"),
},
},
defaultVariants: {
variant: "default",
size: "md",
borderAccent: "none",
status,
},
});
const getStatusIcon = (
status: Props["status"],
): { name: string; class: string } => {
const baseClass = "text-current";
switch (status) {
case "info":
return { name: "InformationCircleIcon", class: baseClass };
case "success":
return { name: "CheckCircleIcon", class: baseClass };
case "warning":
return { name: "ExclamationTriangleIcon", class: baseClass };
case "error":
return { name: "XCircleIcon", class: baseClass };
default:
return { name: "InformationCircleIcon", class: baseClass };
}
};
const finalIcon = icon
? typeof icon === "string"
? { name: icon, class: "" }
: icon
: status
? getStatusIcon(status)
: undefined;
const finalRightIcon = rightIcon
? typeof rightIcon === "string"
? { name: rightIcon, class: "" }
: rightIcon
: undefined;
---
<!-- Component Template -->
<div
class={twMerge(
alertStyles({ variant, borderAccent, size, status }),
className,
)}
role="alert"
aria-live="polite"
aria-labelledby={title ? "alert-title" : undefined}
>
{
finalIcon && iconPosition === "left" && (
<Icon
name={finalIcon.name}
class={twMerge(
`mr-3 ${
size === "sm" ? "w-4 h-4" : size === "lg" ? "w-6 h-6" : "w-5 h-5"
}`,
finalIcon.class,
)}
aria-hidden="true"
/>
)
}
<div class="flex-1">
{
title && (
<h3
id="alert-title"
class={twMerge(
"font-semibold -mt-1 mb-1 block",
size === "sm"
? "text-base"
: size === "md"
? "text-base"
: "text-lg",
)}
>
{title}
</h3>
)
}
{
content && (
<p>
<Fragment set:html={content} />
</p>
)
}
<slot />
</div>
{
(finalIcon && iconPosition === "right") || finalRightIcon ? (
<div class="flex items-center ml-3">
{finalIcon && iconPosition === "right" && (
<Icon
name={finalIcon.name}
class={twMerge(
`mt-0.5 ${
size === "sm"
? "w-4 h-4"
: size === "lg"
? "w-6 h-6"
: "w-5 h-5"
}`,
finalIcon.class,
)}
aria-hidden="true"
/>
)}
{finalRightIcon && (
<Icon
name={finalRightIcon.name}
class={twMerge(
`ml-2 mt-0.5 ${
size === "sm"
? "w-4 h-4"
: size === "lg"
? "w-6 h-6"
: "w-5 h-5"
}`,
finalRightIcon.class,
)}
aria-hidden="true"
/>
)}
</div>
) : null
}
{
dismissible && (
<button
type="button"
class={twMerge(
`ml-auto -mx-1.5 -my-1.5 rounded-lg focus:ring-2 p-1.5 inline-flex items-center justify-center transition-colors`,
`focus:ring-${color}-400 hover:bg-${color}-200 hover:text-${color}-900`,
)}
aria-label="Dismiss"
>
<Icon
name="XMarkIcon"
class={twMerge(
size === "sm" ? "w-4 h-4" : size === "lg" ? "w-6 h-6" : "w-5 h-5",
)}
aria-hidden="true"
/>
</button>
)
}
</div>
<div
class="flex items-start bg-slate-100 dark:bg-slate-800 text-slate-700 dark:text-slate-100 border-slate-200 dark:border-slate-600 border rounded-lg p-4 text-sm"
role="alert"
aria-live="polite">
<div class="flex-1">Slate alert</div>
</div>
<div
class="flex items-start bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-100 border-gray-200 dark:border-gray-600 border rounded-lg p-4 text-sm mt-2"
role="alert"
aria-live="polite">
<div class="flex-1">Gray alert</div>
</div>
<div
class="flex items-start bg-zinc-100 dark:bg-zinc-800 text-zinc-700 dark:text-zinc-100 border-zinc-200 dark:border-zinc-600 border rounded-lg p-4 text-sm mt-2"
role="alert"
aria-live="polite">
<div class="flex-1">Zinc alert</div>
</div>
<div
class="flex items-start bg-neutral-100 dark:bg-neutral-800 text-neutral-700 dark:text-neutral-100 border-neutral-200 dark:border-neutral-600 border rounded-lg p-4 text-sm mt-2"
role="alert"
aria-live="polite">
<div class="flex-1">Neutral alert</div>
</div>
<div
class="flex items-start bg-stone-100 dark:bg-stone-800 text-stone-700 dark:text-stone-100 border-stone-200 dark:border-stone-600 border rounded-lg p-4 text-sm mt-2"
role="alert"
aria-live="polite">
<div class="flex-1">Stone alert</div>
</div>
<div
class="flex items-start bg-red-100 dark:bg-red-800 text-red-700 dark:text-red-100 border-red-200 dark:border-red-600 border rounded-lg p-4 text-sm mt-2"
role="alert"
aria-live="polite">
<div class="flex-1">Red alert</div>
</div>
<div
class="flex items-start bg-orange-100 dark:bg-orange-800 text-orange-700 dark:text-orange-100 border-orange-200 dark:border-orange-600 border rounded-lg p-4 text-sm mt-2"
role="alert"
aria-live="polite">
<div class="flex-1">Orange alert</div>
</div>
<div
class="flex items-start bg-amber-100 dark:bg-amber-800 text-amber-700 dark:text-amber-100 border-amber-200 dark:border-amber-600 border rounded-lg p-4 text-sm mt-2"
role="alert"
aria-live="polite">
<div class="flex-1">Amber alert</div>
</div>
<div
class="flex items-start bg-yellow-100 dark:bg-yellow-800 text-yellow-700 dark:text-yellow-100 border-yellow-200 dark:border-yellow-600 border rounded-lg p-4 text-sm mt-2"
role="alert"
aria-live="polite">
<div class="flex-1">Yellow alert</div>
</div>
<div
class="flex items-start bg-lime-100 dark:bg-lime-800 text-lime-700 dark:text-lime-100 border-lime-200 dark:border-lime-600 border rounded-lg p-4 text-sm mt-2"
role="alert"
aria-live="polite">
<div class="flex-1">Lime alert</div>
</div>
<div
class="flex items-start bg-green-100 dark:bg-green-800 text-green-700 dark:text-green-100 border-green-200 dark:border-green-600 border rounded-lg p-4 text-sm mt-2"
role="alert"
aria-live="polite">
<div class="flex-1">Green alert</div>
</div>
<div
class="flex items-start bg-emerald-100 dark:bg-emerald-800 text-emerald-700 dark:text-emerald-100 border-emerald-200 dark:border-emerald-600 border rounded-lg p-4 text-sm mt-2"
role="alert"
aria-live="polite">
<div class="flex-1">Emerald alert</div>
</div>
<div
class="flex items-start bg-teal-100 dark:bg-teal-800 text-teal-700 dark:text-teal-100 border-teal-200 dark:border-teal-600 border rounded-lg p-4 text-sm mt-2"
role="alert"
aria-live="polite">
<div class="flex-1">Teal alert</div>
</div>
<div
class="flex items-start bg-cyan-100 dark:bg-cyan-800 text-cyan-700 dark:text-cyan-100 border-cyan-200 dark:border-cyan-600 border rounded-lg p-4 text-sm mt-2"
role="alert"
aria-live="polite">
<div class="flex-1">Cyan alert</div>
</div>
<div
class="flex items-start bg-sky-100 dark:bg-sky-800 text-sky-700 dark:text-sky-100 border-sky-200 dark:border-sky-600 border rounded-lg p-4 text-sm mt-2"
role="alert"
aria-live="polite">
<div class="flex-1">Sky alert</div>
</div>
<div
class="flex items-start bg-blue-100 dark:bg-blue-800 text-blue-700 dark:text-blue-100 border-blue-200 dark:border-blue-600 border rounded-lg p-4 text-sm mt-2"
role="alert"
aria-live="polite">
<div class="flex-1">Blue alert</div>
</div>
<div
class="flex items-start bg-indigo-100 dark:bg-indigo-800 text-indigo-700 dark:text-indigo-100 border-indigo-200 dark:border-indigo-600 border rounded-lg p-4 text-sm mt-2"
role="alert"
aria-live="polite">
<div class="flex-1">Indigo alert</div>
</div>
<div
class="flex items-start bg-violet-100 dark:bg-violet-800 text-violet-700 dark:text-violet-100 border-violet-200 dark:border-violet-600 border rounded-lg p-4 text-sm mt-2"
role="alert"
aria-live="polite">
<div class="flex-1">Violet alert</div>
</div>
<div
class="flex items-start bg-purple-100 dark:bg-purple-800 text-purple-700 dark:text-purple-100 border-purple-200 dark:border-purple-600 border rounded-lg p-4 text-sm mt-2"
role="alert"
aria-live="polite">
<div class="flex-1">Purple alert</div>
</div>
<div
class="flex items-start bg-fuchsia-100 dark:bg-fuchsia-800 text-fuchsia-700 dark:text-fuchsia-100 border-fuchsia-200 dark:border-fuchsia-600 border rounded-lg p-4 text-sm mt-2"
role="alert"
aria-live="polite">
<div class="flex-1">Fuchsia alert</div>
</div>
<div
class="flex items-start bg-pink-100 dark:bg-pink-800 text-pink-700 dark:text-pink-100 border-pink-200 dark:border-pink-600 border rounded-lg p-4 text-sm mt-2"
role="alert"
aria-live="polite">
<div class="flex-1">Pink alert</div>
</div>
<div
class="flex items-start bg-rose-100 dark:bg-rose-800 text-rose-700 dark:text-rose-100 border-rose-200 dark:border-rose-600 border rounded-lg p-4 text-sm mt-2"
role="alert"
aria-live="polite">
<div class="flex-1">Rose alert</div>
</div>
---
import { Alert } from '@/components/ui/alert';
---
<Alert color="slate">Slate alert</Alert>
<Alert color="gray">Gray alert</Alert>
<Alert color="zinc">Zinc alert</Alert>
<Alert color="neutral">Neutral alert</Alert>
<Alert color="stone">Stone alert</Alert>
<Alert color="red">Red alert</Alert>
<Alert color="orange">Orange alert</Alert>
<Alert color="amber">Amber alert</Alert>
<Alert color="yellow">Yellow alert</Alert>
<Alert color="lime">Lime alert</Alert>
<Alert color="green">Green alert</Alert>
<Alert color="emerald">Emerald alert</Alert>
<Alert color="teal">Teal alert</Alert>
<Alert color="cyan">Cyan alert</Alert>
<Alert color="sky">Sky alert</Alert>
<Alert color="blue">Blue alert</Alert>
<Alert color="indigo">Indigo alert</Alert>
<Alert color="violet">Violet alert</Alert>
<Alert color="purple">Purple alert</Alert>
<Alert color="fuchsia">Fuchsia alert</Alert>
<Alert color="pink">Pink alert</Alert>
<Alert color="rose">Rose alert</Alert> ---
// Imports
import { type HTMLAttributes } from "astro/types";
import { tv, type VariantProps } from "@utils/custom-tv";
import Icon from "./Icon.astro";
import {
getOutlinedClasses,
type ColorName,
getDefaultClasses,
getSoftClasses,
colorPalette,
} from "@utils/colorUtils";
import { twMerge } from "tailwind-merge";
// PropTypes for documentation
export const propTypes = {
title: { type: "string", description: "The title of the alert" },
content: {
type: "string | JSX.Element",
description: "The content of the alert",
},
icon: {
type: "string | { name: string; class?: string }",
description: "The icon to display in the alert",
},
rightIcon: {
type: "string | { name: string; class?: string }",
description: "The icon to display on the right side of the alert",
},
iconPosition: {
type: ["left", "right"],
description: "The position of the icon",
default: "left",
},
class: {
type: "string",
description: "Additional CSS classes to apply to the alert",
},
dismissible: {
type: "boolean",
description: "Whether the alert is dismissible",
default: false,
},
variant: {
type: ["filled", "outline", "default"],
description: "The variant of the alert",
default: "default",
},
color: {
type: Object.keys(colorPalette),
description: "The color scheme for the alert",
default: "blue",
},
status: {
type: ["info", "success", "warning", "error"],
description: "The status of the alert",
},
borderAccent: {
type: ["top", "left", "right", "bottom", "none", "square"],
description: "The border accent position or shape",
default: "none",
},
size: {
type: ["sm", "md", "lg"],
description: "The size of the alert",
default: "md",
},
};
// Types and Interfaces
type AlertVariants = VariantProps<typeof alertStyles>;
interface Props extends HTMLAttributes<"div">, AlertVariants {
/**
* The title of the alert.
*/
title?: string;
/**
* The content of the alert.
*/
content?: string | JSX.Element;
/**
* The icon to display in the alert.
*/
icon?: string | { name: string; class?: string };
/**
* The icon to display on the right side of the alert.
*/
rightIcon?: string | { name: string; class?: string };
/**
* The position of the icon.
*/
iconPosition?: "left" | "right";
/**
* Additional CSS classes to apply to the alert.
*/
class?: string;
/**
* Whether the alert is dismissible.
*/
dismissible?: boolean;
/**
* The variant of the alert.
*/
variant?: "filled" | "outline" | "default";
/**
* The color scheme for the alert.
*/
color?: ColorName;
/**
* The status of the alert.
*/
status?: "info" | "success" | "warning" | "error";
/**
* The border accent position or shape.
*/
borderAccent?: "top" | "left" | "right" | "bottom" | "none" | "square";
/**
* The size of the alert.
*/
size?: "sm" | "md" | "lg";
}
// Component Logic
const {
title,
content,
icon,
rightIcon,
iconPosition = "left",
variant = "default",
status,
color = "blue",
dismissible = false,
borderAccent = "none",
class: className = "",
size = "md",
} = Astro.props as Props;
// Styles
const alertStyles = tv({
base: "flex items-start border",
variants: {
variant: {
filled: getDefaultClasses(color, { includeHover: false }),
outline: `${getOutlinedClasses(color, { includeHover: false })} border`,
default: `${getSoftClasses(color, { includeHover: false })} border`,
},
borderAccent: {
top: "rounded-b-lg border-t-4",
left: "rounded-r-lg border-l-4",
right: "rounded-l-lg border-r-4",
bottom: "rounded-t-lg border-b-4",
none: "rounded-lg",
square: "", // No rounded corners
},
size: {
sm: "p-3 text-xs",
md: "p-4 text-sm",
lg: "p-6 text-base",
},
status: {
info: getSoftClasses("blue"),
success: getSoftClasses("green"),
warning: getSoftClasses("yellow"),
error: getSoftClasses("red"),
},
},
defaultVariants: {
variant: "default",
size: "md",
borderAccent: "none",
status,
},
});
const getStatusIcon = (
status: Props["status"],
): { name: string; class: string } => {
const baseClass = "text-current";
switch (status) {
case "info":
return { name: "InformationCircleIcon", class: baseClass };
case "success":
return { name: "CheckCircleIcon", class: baseClass };
case "warning":
return { name: "ExclamationTriangleIcon", class: baseClass };
case "error":
return { name: "XCircleIcon", class: baseClass };
default:
return { name: "InformationCircleIcon", class: baseClass };
}
};
const finalIcon = icon
? typeof icon === "string"
? { name: icon, class: "" }
: icon
: status
? getStatusIcon(status)
: undefined;
const finalRightIcon = rightIcon
? typeof rightIcon === "string"
? { name: rightIcon, class: "" }
: rightIcon
: undefined;
---
<!-- Component Template -->
<div
class={twMerge(
alertStyles({ variant, borderAccent, size, status }),
className,
)}
role="alert"
aria-live="polite"
aria-labelledby={title ? "alert-title" : undefined}
>
{
finalIcon && iconPosition === "left" && (
<Icon
name={finalIcon.name}
class={twMerge(
`mr-3 ${
size === "sm" ? "w-4 h-4" : size === "lg" ? "w-6 h-6" : "w-5 h-5"
}`,
finalIcon.class,
)}
aria-hidden="true"
/>
)
}
<div class="flex-1">
{
title && (
<h3
id="alert-title"
class={twMerge(
"font-semibold -mt-1 mb-1 block",
size === "sm"
? "text-base"
: size === "md"
? "text-base"
: "text-lg",
)}
>
{title}
</h3>
)
}
{
content && (
<p>
<Fragment set:html={content} />
</p>
)
}
<slot />
</div>
{
(finalIcon && iconPosition === "right") || finalRightIcon ? (
<div class="flex items-center ml-3">
{finalIcon && iconPosition === "right" && (
<Icon
name={finalIcon.name}
class={twMerge(
`mt-0.5 ${
size === "sm"
? "w-4 h-4"
: size === "lg"
? "w-6 h-6"
: "w-5 h-5"
}`,
finalIcon.class,
)}
aria-hidden="true"
/>
)}
{finalRightIcon && (
<Icon
name={finalRightIcon.name}
class={twMerge(
`ml-2 mt-0.5 ${
size === "sm"
? "w-4 h-4"
: size === "lg"
? "w-6 h-6"
: "w-5 h-5"
}`,
finalRightIcon.class,
)}
aria-hidden="true"
/>
)}
</div>
) : null
}
{
dismissible && (
<button
type="button"
class={twMerge(
`ml-auto -mx-1.5 -my-1.5 rounded-lg focus:ring-2 p-1.5 inline-flex items-center justify-center transition-colors`,
`focus:ring-${color}-400 hover:bg-${color}-200 hover:text-${color}-900`,
)}
aria-label="Dismiss"
>
<Icon
name="XMarkIcon"
class={twMerge(
size === "sm" ? "w-4 h-4" : size === "lg" ? "w-6 h-6" : "w-5 h-5",
)}
aria-hidden="true"
/>
</button>
)
}
</div>
<div
class="flex items-start bg-transparent text-slate-500 dark:text-slate-400 border-slate-500 dark:border-slate-400 border rounded-lg p-4 text-sm"
role="alert"
aria-live="polite">
<div class="flex-1">Slate alert</div>
</div>
<div
class="flex items-start bg-transparent text-gray-500 dark:text-gray-400 border-gray-500 dark:border-gray-400 border rounded-lg p-4 text-sm mt-2"
role="alert"
aria-live="polite">
<div class="flex-1">Gray alert</div>
</div>
<div
class="flex items-start bg-transparent text-zinc-500 dark:text-zinc-400 border-zinc-500 dark:border-zinc-400 border rounded-lg p-4 text-sm mt-2"
role="alert"
aria-live="polite">
<div class="flex-1">Zinc alert</div>
</div>
<div
class="flex items-start bg-transparent text-neutral-500 dark:text-neutral-400 border-neutral-500 dark:border-neutral-400 border rounded-lg p-4 text-sm mt-2"
role="alert"
aria-live="polite">
<div class="flex-1">Neutral alert</div>
</div>
<div
class="flex items-start bg-transparent text-stone-500 dark:text-stone-400 border-stone-500 dark:border-stone-400 border rounded-lg p-4 text-sm mt-2"
role="alert"
aria-live="polite">
<div class="flex-1">Stone alert</div>
</div>
<div
class="flex items-start bg-transparent text-red-500 dark:text-red-400 border-red-500 dark:border-red-400 border rounded-lg p-4 text-sm mt-2"
role="alert"
aria-live="polite">
<div class="flex-1">Red alert</div>
</div>
<div
class="flex items-start bg-transparent text-orange-500 dark:text-orange-400 border-orange-500 dark:border-orange-400 border rounded-lg p-4 text-sm mt-2"
role="alert"
aria-live="polite">
<div class="flex-1">Orange alert</div>
</div>
<div
class="flex items-start bg-transparent text-amber-500 dark:text-amber-4000 border-amber-500 dark:border-amber-4000 border rounded-lg p-4 text-sm mt-2"
role="alert"
aria-live="polite">
<div class="flex-1">Amber alert</div>
</div>
<div
class="flex items-start bg-transparent text-yellow-500 dark:text-yellow-400 border-yellow-500 dark:border-yellow-400 border rounded-lg p-4 text-sm mt-2"
role="alert"
aria-live="polite">
<div class="flex-1">Yellow alert</div>
</div>
<div
class="flex items-start bg-transparent text-lime-500 dark:text-lime-400 border-lime-500 dark:border-lime-400 border rounded-lg p-4 text-sm mt-2"
role="alert"
aria-live="polite">
<div class="flex-1">Lime alert</div>
</div>
<div
class="flex items-start bg-transparent text-green-500 dark:text-green-400 border-green-500 dark:border-green-400 border rounded-lg p-4 text-sm mt-2"
role="alert"
aria-live="polite">
<div class="flex-1">Green alert</div>
</div>
<div
class="flex items-start bg-transparent text-emerald-500 dark:text-emerald-400 border-emerald-500 dark:border-emerald-400 border rounded-lg p-4 text-sm mt-2"
role="alert"
aria-live="polite">
<div class="flex-1">Emerald alert</div>
</div>
<div
class="flex items-start bg-transparent text-teal-500 dark:text-teal-400 border-teal-500 dark:border-teal-400 border rounded-lg p-4 text-sm mt-2"
role="alert"
aria-live="polite">
<div class="flex-1">Teal alert</div>
</div>
<div
class="flex items-start bg-transparent text-cyan-500 dark:text-cyan-400 border-cyan-500 dark:border-cyan-400 border rounded-lg p-4 text-sm mt-2"
role="alert"
aria-live="polite">
<div class="flex-1">Cyan alert</div>
</div>
<div
class="flex items-start bg-transparent text-sky-500 dark:text-sky-400 border-sky-500 dark:border-sky-400 border rounded-lg p-4 text-sm mt-2"
role="alert"
aria-live="polite">
<div class="flex-1">Sky alert</div>
</div>
<div
class="flex items-start bg-transparent text-blue-500 dark:text-blue-400 border-blue-500 dark:border-blue-400 border rounded-lg p-4 text-sm mt-2"
role="alert"
aria-live="polite">
<div class="flex-1">Blue alert</div>
</div>
<div
class="flex items-start bg-transparent text-indigo-500 dark:text-indigo-400 border-indigo-500 dark:border-indigo-400 border rounded-lg p-4 text-sm mt-2"
role="alert"
aria-live="polite">
<div class="flex-1">Indigo alert</div>
</div>
<div
class="flex items-start bg-transparent text-violet-500 dark:text-violet-400 border-violet-500 dark:border-violet-400 border rounded-lg p-4 text-sm mt-2"
role="alert"
aria-live="polite">
<div class="flex-1">Violet alert</div>
</div>
<div
class="flex items-start bg-transparent text-purple-500 dark:text-purple-400 border-purple-500 dark:border-purple-400 border rounded-lg p-4 text-sm mt-2"
role="alert"
aria-live="polite">
<div class="flex-1">Purple alert</div>
</div>
<div
class="flex items-start bg-transparent text-fuchsia-500 dark:text-fuchsia-400 border-fuchsia-500 dark:border-fuchsia-400 border rounded-lg p-4 text-sm mt-2"
role="alert"
aria-live="polite">
<div class="flex-1">Fuchsia alert</div>
</div>
<div
class="flex items-start bg-transparent text-pink-500 dark:text-pink-400 border-pink-500 dark:border-pink-400 border rounded-lg p-4 text-sm mt-2"
role="alert"
aria-live="polite">
<div class="flex-1">Pink alert</div>
</div>
<div
class="flex items-start bg-transparent text-rose-500 dark:text-rose-400 border-rose-500 dark:border-rose-400 border rounded-lg p-4 text-sm mt-2"
role="alert"
aria-live="polite">
<div class="flex-1">Rose alert</div>
</div>
---
import { Alert } from '@/components/ui/alert';
---
<Alert color="slate" variant="outline">Slate alert</Alert>
<Alert color="gray" variant="outline">Gray alert</Alert>
<Alert color="zinc" variant="outline">Zinc alert</Alert>
<Alert color="neutral" variant="outline">Neutral alert</Alert>
<Alert color="stone" variant="outline">Stone alert</Alert>
<Alert color="red" variant="outline">Red alert</Alert>
<Alert color="orange" variant="outline">Orange alert</Alert>
<Alert color="amber" variant="outline">Amber alert</Alert>
<Alert color="yellow" variant="outline">Yellow alert</Alert>
<Alert color="lime" variant="outline">Lime alert</Alert>
<Alert color="green" variant="outline">Green alert</Alert>
<Alert color="emerald" variant="outline">Emerald alert</Alert>
<Alert color="teal" variant="outline">Teal alert</Alert>
<Alert color="cyan" variant="outline">Cyan alert</Alert>
<Alert color="sky" variant="outline">Sky alert</Alert>
<Alert color="blue" variant="outline">Blue alert</Alert>
<Alert color="indigo" variant="outline">Indigo alert</Alert>
<Alert color="violet" variant="outline">Violet alert</Alert>
<Alert color="purple" variant="outline">Purple alert</Alert>
<Alert color="fuchsia" variant="outline">Fuchsia alert</Alert>
<Alert color="pink" variant="outline">Pink alert</Alert>
<Alert color="rose" variant="outline">Rose alert</Alert> ---
// Imports
import { type HTMLAttributes } from "astro/types";
import { tv, type VariantProps } from "@utils/custom-tv";
import Icon from "./Icon.astro";
import {
getOutlinedClasses,
type ColorName,
getDefaultClasses,
getSoftClasses,
colorPalette,
} from "@utils/colorUtils";
import { twMerge } from "tailwind-merge";
// PropTypes for documentation
export const propTypes = {
title: { type: "string", description: "The title of the alert" },
content: {
type: "string | JSX.Element",
description: "The content of the alert",
},
icon: {
type: "string | { name: string; class?: string }",
description: "The icon to display in the alert",
},
rightIcon: {
type: "string | { name: string; class?: string }",
description: "The icon to display on the right side of the alert",
},
iconPosition: {
type: ["left", "right"],
description: "The position of the icon",
default: "left",
},
class: {
type: "string",
description: "Additional CSS classes to apply to the alert",
},
dismissible: {
type: "boolean",
description: "Whether the alert is dismissible",
default: false,
},
variant: {
type: ["filled", "outline", "default"],
description: "The variant of the alert",
default: "default",
},
color: {
type: Object.keys(colorPalette),
description: "The color scheme for the alert",
default: "blue",
},
status: {
type: ["info", "success", "warning", "error"],
description: "The status of the alert",
},
borderAccent: {
type: ["top", "left", "right", "bottom", "none", "square"],
description: "The border accent position or shape",
default: "none",
},
size: {
type: ["sm", "md", "lg"],
description: "The size of the alert",
default: "md",
},
};
// Types and Interfaces
type AlertVariants = VariantProps<typeof alertStyles>;
interface Props extends HTMLAttributes<"div">, AlertVariants {
/**
* The title of the alert.
*/
title?: string;
/**
* The content of the alert.
*/
content?: string | JSX.Element;
/**
* The icon to display in the alert.
*/
icon?: string | { name: string; class?: string };
/**
* The icon to display on the right side of the alert.
*/
rightIcon?: string | { name: string; class?: string };
/**
* The position of the icon.
*/
iconPosition?: "left" | "right";
/**
* Additional CSS classes to apply to the alert.
*/
class?: string;
/**
* Whether the alert is dismissible.
*/
dismissible?: boolean;
/**
* The variant of the alert.
*/
variant?: "filled" | "outline" | "default";
/**
* The color scheme for the alert.
*/
color?: ColorName;
/**
* The status of the alert.
*/
status?: "info" | "success" | "warning" | "error";
/**
* The border accent position or shape.
*/
borderAccent?: "top" | "left" | "right" | "bottom" | "none" | "square";
/**
* The size of the alert.
*/
size?: "sm" | "md" | "lg";
}
// Component Logic
const {
title,
content,
icon,
rightIcon,
iconPosition = "left",
variant = "default",
status,
color = "blue",
dismissible = false,
borderAccent = "none",
class: className = "",
size = "md",
} = Astro.props as Props;
// Styles
const alertStyles = tv({
base: "flex items-start border",
variants: {
variant: {
filled: getDefaultClasses(color, { includeHover: false }),
outline: `${getOutlinedClasses(color, { includeHover: false })} border`,
default: `${getSoftClasses(color, { includeHover: false })} border`,
},
borderAccent: {
top: "rounded-b-lg border-t-4",
left: "rounded-r-lg border-l-4",
right: "rounded-l-lg border-r-4",
bottom: "rounded-t-lg border-b-4",
none: "rounded-lg",
square: "", // No rounded corners
},
size: {
sm: "p-3 text-xs",
md: "p-4 text-sm",
lg: "p-6 text-base",
},
status: {
info: getSoftClasses("blue"),
success: getSoftClasses("green"),
warning: getSoftClasses("yellow"),
error: getSoftClasses("red"),
},
},
defaultVariants: {
variant: "default",
size: "md",
borderAccent: "none",
status,
},
});
const getStatusIcon = (
status: Props["status"],
): { name: string; class: string } => {
const baseClass = "text-current";
switch (status) {
case "info":
return { name: "InformationCircleIcon", class: baseClass };
case "success":
return { name: "CheckCircleIcon", class: baseClass };
case "warning":
return { name: "ExclamationTriangleIcon", class: baseClass };
case "error":
return { name: "XCircleIcon", class: baseClass };
default:
return { name: "InformationCircleIcon", class: baseClass };
}
};
const finalIcon = icon
? typeof icon === "string"
? { name: icon, class: "" }
: icon
: status
? getStatusIcon(status)
: undefined;
const finalRightIcon = rightIcon
? typeof rightIcon === "string"
? { name: rightIcon, class: "" }
: rightIcon
: undefined;
---
<!-- Component Template -->
<div
class={twMerge(
alertStyles({ variant, borderAccent, size, status }),
className,
)}
role="alert"
aria-live="polite"
aria-labelledby={title ? "alert-title" : undefined}
>
{
finalIcon && iconPosition === "left" && (
<Icon
name={finalIcon.name}
class={twMerge(
`mr-3 ${
size === "sm" ? "w-4 h-4" : size === "lg" ? "w-6 h-6" : "w-5 h-5"
}`,
finalIcon.class,
)}
aria-hidden="true"
/>
)
}
<div class="flex-1">
{
title && (
<h3
id="alert-title"
class={twMerge(
"font-semibold -mt-1 mb-1 block",
size === "sm"
? "text-base"
: size === "md"
? "text-base"
: "text-lg",
)}
>
{title}
</h3>
)
}
{
content && (
<p>
<Fragment set:html={content} />
</p>
)
}
<slot />
</div>
{
(finalIcon && iconPosition === "right") || finalRightIcon ? (
<div class="flex items-center ml-3">
{finalIcon && iconPosition === "right" && (
<Icon
name={finalIcon.name}
class={twMerge(
`mt-0.5 ${
size === "sm"
? "w-4 h-4"
: size === "lg"
? "w-6 h-6"
: "w-5 h-5"
}`,
finalIcon.class,
)}
aria-hidden="true"
/>
)}
{finalRightIcon && (
<Icon
name={finalRightIcon.name}
class={twMerge(
`ml-2 mt-0.5 ${
size === "sm"
? "w-4 h-4"
: size === "lg"
? "w-6 h-6"
: "w-5 h-5"
}`,
finalRightIcon.class,
)}
aria-hidden="true"
/>
)}
</div>
) : null
}
{
dismissible && (
<button
type="button"
class={twMerge(
`ml-auto -mx-1.5 -my-1.5 rounded-lg focus:ring-2 p-1.5 inline-flex items-center justify-center transition-colors`,
`focus:ring-${color}-400 hover:bg-${color}-200 hover:text-${color}-900`,
)}
aria-label="Dismiss"
>
<Icon
name="XMarkIcon"
class={twMerge(
size === "sm" ? "w-4 h-4" : size === "lg" ? "w-6 h-6" : "w-5 h-5",
)}
aria-hidden="true"
/>
</button>
)
}
</div>
<div
class="flex items-start bg-blue-100 dark:bg-blue-800 text-blue-700 dark:text-blue-100 border-blue-200 dark:border-blue-600 border rounded-lg p-4 text-sm"
role="alert"
aria-live="polite"
aria-labelledby="alert-title">
<span class="inline-block size-5 mr-3 w-5 h-5">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
aria-hidden="true"
data-slot="icon">
<path
fill-rule="evenodd"
d="M2.25 12c0-5.385 4.365-9.75 9.75-9.75s9.75 4.365 9.75 9.75-4.365 9.75-9.75 9.75S2.25 17.385 2.25 12Zm8.706-1.442c1.146-.573 2.437.463 2.126 1.706l-.709 2.836.042-.02a.75.75 0 0 1 .67 1.34l-.04.022c-1.147.573-2.438-.463-2.127-1.706l.71-2.836-.042.02a.75.75 0 1 1-.671-1.34l.041-.022ZM12 9a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5Z"
clip-rule="evenodd"></path>
</svg>
</span>
<div class="flex-1">
<h3 id="alert-title" class="font-semibold -mt-1 mb-1 block text-base">Important Information</h3>
<ul class="list-disc list-inside">
<li>First item in the list</li>
<li>Second item in the list</li>
<li>Third item in the list</li>
</ul>
</div>
</div>
---
import { Alert } from '@/components/ui/alert';
---
<Alert
title="Important Information"
icon="InformationCircleIcon"
>
<ul class="list-disc list-inside">
<li>First item in the list</li>
<li>Second item in the list</li>
<li>Third item in the list</li>
</ul>
</Alert> ---
// Imports
import { type HTMLAttributes } from "astro/types";
import { tv, type VariantProps } from "@utils/custom-tv";
import Icon from "./Icon.astro";
import {
getOutlinedClasses,
type ColorName,
getDefaultClasses,
getSoftClasses,
colorPalette,
} from "@utils/colorUtils";
import { twMerge } from "tailwind-merge";
// PropTypes for documentation
export const propTypes = {
title: { type: "string", description: "The title of the alert" },
content: {
type: "string | JSX.Element",
description: "The content of the alert",
},
icon: {
type: "string | { name: string; class?: string }",
description: "The icon to display in the alert",
},
rightIcon: {
type: "string | { name: string; class?: string }",
description: "The icon to display on the right side of the alert",
},
iconPosition: {
type: ["left", "right"],
description: "The position of the icon",
default: "left",
},
class: {
type: "string",
description: "Additional CSS classes to apply to the alert",
},
dismissible: {
type: "boolean",
description: "Whether the alert is dismissible",
default: false,
},
variant: {
type: ["filled", "outline", "default"],
description: "The variant of the alert",
default: "default",
},
color: {
type: Object.keys(colorPalette),
description: "The color scheme for the alert",
default: "blue",
},
status: {
type: ["info", "success", "warning", "error"],
description: "The status of the alert",
},
borderAccent: {
type: ["top", "left", "right", "bottom", "none", "square"],
description: "The border accent position or shape",
default: "none",
},
size: {
type: ["sm", "md", "lg"],
description: "The size of the alert",
default: "md",
},
};
// Types and Interfaces
type AlertVariants = VariantProps<typeof alertStyles>;
interface Props extends HTMLAttributes<"div">, AlertVariants {
/**
* The title of the alert.
*/
title?: string;
/**
* The content of the alert.
*/
content?: string | JSX.Element;
/**
* The icon to display in the alert.
*/
icon?: string | { name: string; class?: string };
/**
* The icon to display on the right side of the alert.
*/
rightIcon?: string | { name: string; class?: string };
/**
* The position of the icon.
*/
iconPosition?: "left" | "right";
/**
* Additional CSS classes to apply to the alert.
*/
class?: string;
/**
* Whether the alert is dismissible.
*/
dismissible?: boolean;
/**
* The variant of the alert.
*/
variant?: "filled" | "outline" | "default";
/**
* The color scheme for the alert.
*/
color?: ColorName;
/**
* The status of the alert.
*/
status?: "info" | "success" | "warning" | "error";
/**
* The border accent position or shape.
*/
borderAccent?: "top" | "left" | "right" | "bottom" | "none" | "square";
/**
* The size of the alert.
*/
size?: "sm" | "md" | "lg";
}
// Component Logic
const {
title,
content,
icon,
rightIcon,
iconPosition = "left",
variant = "default",
status,
color = "blue",
dismissible = false,
borderAccent = "none",
class: className = "",
size = "md",
} = Astro.props as Props;
// Styles
const alertStyles = tv({
base: "flex items-start border",
variants: {
variant: {
filled: getDefaultClasses(color, { includeHover: false }),
outline: `${getOutlinedClasses(color, { includeHover: false })} border`,
default: `${getSoftClasses(color, { includeHover: false })} border`,
},
borderAccent: {
top: "rounded-b-lg border-t-4",
left: "rounded-r-lg border-l-4",
right: "rounded-l-lg border-r-4",
bottom: "rounded-t-lg border-b-4",
none: "rounded-lg",
square: "", // No rounded corners
},
size: {
sm: "p-3 text-xs",
md: "p-4 text-sm",
lg: "p-6 text-base",
},
status: {
info: getSoftClasses("blue"),
success: getSoftClasses("green"),
warning: getSoftClasses("yellow"),
error: getSoftClasses("red"),
},
},
defaultVariants: {
variant: "default",
size: "md",
borderAccent: "none",
status,
},
});
const getStatusIcon = (
status: Props["status"],
): { name: string; class: string } => {
const baseClass = "text-current";
switch (status) {
case "info":
return { name: "InformationCircleIcon", class: baseClass };
case "success":
return { name: "CheckCircleIcon", class: baseClass };
case "warning":
return { name: "ExclamationTriangleIcon", class: baseClass };
case "error":
return { name: "XCircleIcon", class: baseClass };
default:
return { name: "InformationCircleIcon", class: baseClass };
}
};
const finalIcon = icon
? typeof icon === "string"
? { name: icon, class: "" }
: icon
: status
? getStatusIcon(status)
: undefined;
const finalRightIcon = rightIcon
? typeof rightIcon === "string"
? { name: rightIcon, class: "" }
: rightIcon
: undefined;
---
<!-- Component Template -->
<div
class={twMerge(
alertStyles({ variant, borderAccent, size, status }),
className,
)}
role="alert"
aria-live="polite"
aria-labelledby={title ? "alert-title" : undefined}
>
{
finalIcon && iconPosition === "left" && (
<Icon
name={finalIcon.name}
class={twMerge(
`mr-3 ${
size === "sm" ? "w-4 h-4" : size === "lg" ? "w-6 h-6" : "w-5 h-5"
}`,
finalIcon.class,
)}
aria-hidden="true"
/>
)
}
<div class="flex-1">
{
title && (
<h3
id="alert-title"
class={twMerge(
"font-semibold -mt-1 mb-1 block",
size === "sm"
? "text-base"
: size === "md"
? "text-base"
: "text-lg",
)}
>
{title}
</h3>
)
}
{
content && (
<p>
<Fragment set:html={content} />
</p>
)
}
<slot />
</div>
{
(finalIcon && iconPosition === "right") || finalRightIcon ? (
<div class="flex items-center ml-3">
{finalIcon && iconPosition === "right" && (
<Icon
name={finalIcon.name}
class={twMerge(
`mt-0.5 ${
size === "sm"
? "w-4 h-4"
: size === "lg"
? "w-6 h-6"
: "w-5 h-5"
}`,
finalIcon.class,
)}
aria-hidden="true"
/>
)}
{finalRightIcon && (
<Icon
name={finalRightIcon.name}
class={twMerge(
`ml-2 mt-0.5 ${
size === "sm"
? "w-4 h-4"
: size === "lg"
? "w-6 h-6"
: "w-5 h-5"
}`,
finalRightIcon.class,
)}
aria-hidden="true"
/>
)}
</div>
) : null
}
{
dismissible && (
<button
type="button"
class={twMerge(
`ml-auto -mx-1.5 -my-1.5 rounded-lg focus:ring-2 p-1.5 inline-flex items-center justify-center transition-colors`,
`focus:ring-${color}-400 hover:bg-${color}-200 hover:text-${color}-900`,
)}
aria-label="Dismiss"
>
<Icon
name="XMarkIcon"
class={twMerge(
size === "sm" ? "w-4 h-4" : size === "lg" ? "w-6 h-6" : "w-5 h-5",
)}
aria-hidden="true"
/>
</button>
)
}
</div>
Component Properties
| Property | Type | Default | Description |
|---|---|---|---|
| title | string | - | The title of the alert |
| content | string | JSX.Element | - | The content of the alert |
| icon | string | { name: string; class?: string } | - | The icon to display in the alert |
| rightIcon | string | { name: string; class?: string } | - | The icon to display on the right side of the alert |
| iconPosition | left | right | "left" | The position of the icon |
| class | string | - | Additional CSS classes to apply to the alert |
| dismissible | boolean | false | Whether the alert is dismissible |
| variant | filled | outline | default | "default" | The variant of the alert |
| 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 alert |
| status | info | success | warning | error | - | The status of the alert |
| borderAccent | top | left | right | bottom | none | square | "none" | The border accent position or shape |
| size | sm | md | lg | "md" | The size of the alert |