Alerts

Informative alert components for displaying messages, warnings, and notifications. Supports various styles, colors, and customization options. previews

Default Alert

A basic alert component for general information.

 

<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>

Alert with Title and Icon

An alert component with a title, content, and icon.

 

<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>

Alert Variants

Alerts with different variants: filled and outline.

 

<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>

Alert Sizes

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

 

<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>

Alert with Border Accent

Alerts with different border accent positions.

 

<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>

Alert with Icon Position

Alerts with different icon positions: left and right.

 

<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>

Dismissible Alert

An alert that can be dismissed by the user.

 

<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>

Filled Alert Colors

Alerts with different color options in filled variant.

 

<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>

Outline Alert Colors

Alerts with different color options in outline variant.

 

<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>

Alert with List

An alert component that contains a list.

 

<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