Bloom Logo

Alert

Displays a clean contextual banner for user attention with theme-adaptive neutral backgrounds, colored titles & icons, and custom slot support.

alert.tsx

Core implementation of the Alert component featuring clean neutral cards, status-colored titles, and custom slots.

ReactTailwindFeedbackUI ComponentAlert
"use client";

import * as React from "react";
import { AlertCircle, CheckCircle2, Info, AlertTriangle, X } from "lucide-react";
import { cn } from "@/lib/utils";

type AlertColor =
  | "default"
  | "primary"
  | "secondary"
  | "accent"
  | "info"
  | "success"
  | "warning"
  | "danger";

type AlertVariant = "default" | "bordered" | "flat" | "ghost" | "shadow";

interface AlertContextValue {
  color: AlertColor;
  variant: AlertVariant;
}

const AlertContext = React.createContext<AlertContextValue>({
  color: "info",
  variant: "default",
});

const useAlertContext = () => React.useContext(AlertContext);

const titleColorMap: Record<AlertColor, string> = {
  default: "text-zinc-900 dark:text-zinc-100 font-semibold",
  primary: "text-sky-600 dark:text-sky-400 font-semibold",
  secondary: "text-purple-600 dark:text-purple-400 font-semibold",
  accent: "text-pink-600 dark:text-pink-400 font-semibold",
  info: "text-sky-600 dark:text-sky-400 font-semibold",
  success: "text-emerald-600 dark:text-emerald-400 font-semibold",
  warning: "text-amber-600 dark:text-amber-400 font-semibold",
  danger: "text-rose-600 dark:text-rose-400 font-semibold",
};

const iconColorMap: Record<AlertColor, string> = {
  default: "text-zinc-500 dark:text-zinc-400",
  primary: "text-sky-500",
  secondary: "text-purple-500",
  accent: "text-pink-500",
  info: "text-sky-500",
  success: "text-emerald-500",
  warning: "text-amber-500",
  danger: "text-rose-500",
};

const variantCardMap: Record<AlertVariant, string> = {
  default: "bg-white dark:bg-zinc-900 text-zinc-900 dark:text-zinc-100 border border-zinc-200/80 dark:border-zinc-800/80 shadow-xs",
  bordered: "bg-transparent text-zinc-900 dark:text-zinc-100 border border-zinc-200 dark:border-zinc-800",
  flat: "bg-zinc-100/90 dark:bg-zinc-900/60 text-zinc-900 dark:text-zinc-100 border border-transparent",
  ghost: "bg-transparent text-zinc-900 dark:text-zinc-100 border border-transparent",
  shadow: "bg-white dark:bg-zinc-900 text-zinc-900 dark:text-zinc-100 border border-zinc-200/60 dark:border-zinc-800/60 shadow-md",
};

const iconMap: Record<AlertColor, React.ElementType> = {
  default: Info,
  primary: Info,
  secondary: Info,
  accent: Info,
  info: Info,
  success: CheckCircle2,
  warning: AlertTriangle,
  danger: AlertCircle,
};

export interface AlertProps
  extends Omit<React.HTMLAttributes<HTMLDivElement>, "title"> {
  variant?: AlertVariant;
  color?: AlertColor;
  title?: React.ReactNode;
  icon?: React.ReactNode;
  startContent?: React.ReactNode;
  endContent?: React.ReactNode;
  hideIcon?: boolean;
  isClosable?: boolean;
  onClose?: () => void;
  action?: React.ReactNode;
}

const Alert = React.forwardRef<HTMLDivElement, AlertProps>(
  (
    {
      className,
      variant = "default",
      color = "info",
      title,
      icon,
      startContent,
      endContent,
      hideIcon = false,
      isClosable = false,
      onClose,
      action,
      children,
      ...props
    },
    ref
  ) => {
    const [isVisible, setIsVisible] = React.useState(true);

    if (!isVisible) return null;

    const handleClose = () => {
      setIsVisible(false);
      onClose?.();
    };

    const IconComponent = iconMap[color];

    const renderedStart = startContent ?? (
      !hideIcon && (
        icon ?? (
          <IconComponent
            className={cn("size-5 shrink-0 mt-0.5", iconColorMap[color])}
          />
        )
      )
    );

    return (
      <AlertContext.Provider value={{ color, variant }}>
        <div
          ref={ref}
          role="alert"
          className={cn(
            "relative w-full rounded-2xl p-4 text-sm flex gap-3.5 items-start leading-relaxed transition-all duration-200",
            variantCardMap[variant],
            className
          )}
          {...props}
        >
          {renderedStart}

          <div className="flex flex-col flex-1 min-w-0">
            {title && <AlertTitle>{title}</AlertTitle>}
            {children && (
              typeof children === "string" ? (
                <AlertDescription>{children}</AlertDescription>
              ) : (
                children
              )
            )}
            {action && <div className="mt-3">{action}</div>}
          </div>

          {(endContent || isClosable) && (
            <div className="flex items-center gap-2 shrink-0 self-start">
              {endContent}
              {isClosable && (
                <button
                  type="button"
                  onClick={handleClose}
                  className="p-1 rounded-lg text-zinc-400 hover:text-zinc-700 dark:hover:text-zinc-200 hover:bg-zinc-200/50 dark:hover:bg-zinc-800/60 transition-colors cursor-pointer"
                  aria-label="Close alert"
                >
                  <X className="size-4" />
                </button>
              )}
            </div>
          )}
        </div>
      </AlertContext.Provider>
    );
  }
);
Alert.displayName = "Alert";

const AlertTitle = React.forwardRef<
  HTMLHeadingElement,
  React.HTMLAttributes<HTMLHeadingElement>
>(({ className, children, ...props }, ref) => {
  const { color } = useAlertContext();

  return (
    <h5
      ref={ref}
      className={cn(
        "font-semibold text-sm leading-none tracking-tight",
        titleColorMap[color],
        className
      )}
      {...props}
    >
      {children}
    </h5>
  );
});
AlertTitle.displayName = "AlertTitle";

const AlertDescription = React.forwardRef<
  HTMLDivElement,
  React.HTMLAttributes<HTMLDivElement>
>(({ className, children, ...props }, ref) => (
  <div
    ref={ref}
    className={cn(
      "text-xs text-zinc-600 dark:text-zinc-400 leading-relaxed mt-1.5",
      className
    )}
    {...props}
  >
    {children}
  </div>
));
AlertDescription.displayName = "AlertDescription";

export { Alert, AlertTitle, AlertDescription };
export type { AlertColor, AlertVariant };

Default

A clean, subtle alert banner with a neutral background, status-colored title, and status icon.

color: infotitle: string

Colors

Select semantic colors via the color prop. The card background stays clean and neutral while only the title and status icon inherit the semantic accent color.

color: default | primary | secondary | accent | info | success | warning | danger

Variants

Control the card framing and depth using the variant prop (default, bordered, flat, ghost, shadow).

variant: default | bordered | flat | ghost | shadow
variant="default"
variant="bordered"
variant="flat"
variant="shadow"

Start & End Slots (startContent & endContent)

Inject custom elements before the title using startContent, or trailing action controls and badges using endContent.

startContent: ReactNodeendContent: ReactNode

Dismissible Alert (isClosable)

Enable dismissible behavior by setting isClosable to true. Handles state internally and triggers onClose callback when dismissed.

isClosable: booleanonClose: () => void

Compound Components (AlertTitle & AlertDescription)

Compose flexible alert layouts using AlertTitle and AlertDescription subcomponents.

API Reference

Props — Alert

Properties for configuring the Alert banner component.

PropTypeDefaultDescription
color'default' | 'primary' | 'secondary' | 'accent' | 'info' | 'success' | 'warning' | 'danger''info'Semantic color applied exclusively to the title header and status icon.
variant'default' | 'bordered' | 'flat' | 'ghost' | 'shadow''default'Visual card style determining background depth and border framing.
titleReactNodeTitle header text rendered with the status color.
startContentReactNodeCustom element (icon, avatar) rendered before the title and message.
endContentReactNodeCustom element (action button, badge) rendered on the right side.
iconReactNodeCustom icon replacing the default status type icon.
hideIconbooleanfalseHides the leading icon completely.
isClosablebooleanfalseRenders a close button on the right to dismiss the alert banner.
onClose() => voidCallback function fired when the alert is dismissed.

Props — AlertTitle

Properties for configuring the AlertTitle component.

PropTypeDefaultDescription
classNamestringCustom CSS class names for styling the title element.

Props — AlertDescription

Properties for configuring the AlertDescription component.

PropTypeDefaultDescription
classNamestringCustom CSS class names for styling the description container.