React SME Cookbook
All FAQs

Search Documentation

Search across all documentation pages

alertbannernotificationmessagestatusfeedbackcomponenttailwind

Alert

An inline banner component that communicates status messages, warnings, or contextual information to the user within the normal page flow.

Use Cases

  • Display a success message after a form submission
  • Warn users about unsaved changes before navigating away
  • Show an error summary at the top of a form with validation failures
  • Inform users about scheduled maintenance or downtime
  • Present a tip or informational note inside documentation content
  • Alert users about account-level issues (billing, verification)
  • Display a deprecation notice for an API or feature

Simplest Implementation

interface AlertProps {
  children: React.ReactNode;
}
 
export function Alert({ children }: AlertProps) {
  return (
    <div role="alert" className="rounded-lg border border-blue-200 bg-blue-50 p-4 text-sm text-blue-800">
      {children}
    </div>
  );
}

A static informational alert with role="alert" so screen readers announce it immediately when it appears in the DOM. No "use client" is needed since there is no interactivity.

Variations

Info Alert

interface AlertProps {
  title?: string;
  children: React.ReactNode;
}
 
export function AlertInfo({ title, children }: AlertProps) {
  return (
    <div role="alert" className="rounded-lg border border-blue-200 bg-blue-50 p-4">
      {title && <p className="mb-1 text-sm font-semibold text-blue-900">{title}</p>}
      <p className="text-sm text-blue-800">{children}</p>
    </div>
  );
}

The info variant uses blue tones to indicate neutral, informational content. The optional title prop adds a bold heading line above the description for two-level messaging.

Success Alert

interface AlertProps {
  title?: string;
  children: React.ReactNode;
}
 
export function AlertSuccess({ title, children }: AlertProps) {
  return (
    <div role="alert" className="rounded-lg border border-green-200 bg-green-50 p-4">
      {title && <p className="mb-1 text-sm font-semibold text-green-900">{title}</p>}
      <p className="text-sm text-green-800">{children}</p>
    </div>
  );
}

Green signals a positive outcome -- form saved, payment processed, account verified. This variant is typically shown after a successful action and may be paired with a dismissible close button.

Warning Alert

interface AlertProps {
  title?: string;
  children: React.ReactNode;
}
 
export function AlertWarning({ title, children }: AlertProps) {
  return (
    <div role="alert" className="rounded-lg border border-yellow-200 bg-yellow-50 p-4">
      {title && <p className="mb-1 text-sm font-semibold text-yellow-900">{title}</p>}
      <p className="text-sm text-yellow-800">{children}</p>
    </div>
  );
}

Yellow draws attention without implying failure. Use this for conditions that need acknowledgment but are not blocking -- low disk space, approaching rate limits, or deprecated features still in use.

Destructive / Error Alert

interface AlertProps {
  title?: string;
  children: React.ReactNode;
}
 
export function AlertError({ title, children }: AlertProps) {
  return (
    <div role="alert" className="rounded-lg border border-red-200 bg-red-50 p-4">
      {title && <p className="mb-1 text-sm font-semibold text-red-900">{title}</p>}
      <p className="text-sm text-red-800">{children}</p>
    </div>
  );
}

Red communicates critical problems that require immediate attention -- validation errors, failed requests, or destructive action confirmations. Consider using aria-live="assertive" for errors that appear dynamically.

With Icon

type AlertVariant = "info" | "success" | "warning" | "error";
 
interface AlertProps {
  variant?: AlertVariant;
  title?: string;
  children: React.ReactNode;
}
 
const variantStyles: Record<AlertVariant, { container: string; icon: string }> = {
  info: {
    container: "border-blue-200 bg-blue-50 text-blue-800",
    icon: "text-blue-500",
  },
  success: {
    container: "border-green-200 bg-green-50 text-green-800",
    icon: "text-green-500",
  },
  warning: {
    container: "border-yellow-200 bg-yellow-50 text-yellow-800",
    icon: "text-yellow-500",
  },
  error: {
    container: "border-red-200 bg-red-50 text-red-800",
    icon: "text-red-500",
  },
};
 
const icons: Record<AlertVariant, React.ReactNode> = {
  info: (
    <svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
      <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
    </svg>
  ),
  success: (
    <svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
      <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
    </svg>
  ),
  warning: (
    <svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
      <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L4.082 16.5c-.77.833.192 2.5 1.732 2.5z" />
    </svg>
  ),
  error: (
    <svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
      <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
    </svg>
  ),
};
 
export function Alert({ variant = "info", title, children }: AlertProps) {
  const styles = variantStyles[variant];
 
  return (
    <div role="alert" className={`flex gap-3 rounded-lg border p-4 ${styles.container}`}>
      <span className={`shrink-0 ${styles.icon}`}>{icons[variant]}</span>
      <div>
        {title && <p className="mb-1 text-sm font-semibold">{title}</p>}
        <p className="text-sm">{children}</p>
      </div>
    </div>
  );
}

Icons reinforce the alert meaning beyond color alone, which is essential for accessibility. The shrink-0 on the icon wrapper prevents it from compressing when the text content is long. The flex layout with gap-3 keeps consistent spacing between icon and text.

Dismissible with Close Button

"use client";
 
import { useState } from "react";
 
type AlertVariant = "info" | "success" | "warning" | "error";
 
interface AlertProps {
  variant?: AlertVariant;
  title?: string;
  children: React.ReactNode;
  onDismiss?: () => void;
}
 
const variantStyles: Record<AlertVariant, string> = {
  info: "border-blue-200 bg-blue-50 text-blue-800",
  success: "border-green-200 bg-green-50 text-green-800",
  warning: "border-yellow-200 bg-yellow-50 text-yellow-800",
  error: "border-red-200 bg-red-50 text-red-800",
};
 
export function DismissibleAlert({ variant = "info", title, children, onDismiss }: AlertProps) {
  const [visible, setVisible] = useState(true);
 
  if (!visible) return null;
 
  function handleDismiss() {
    setVisible(false);
    onDismiss?.();
  }
 
  return (
    <div role="alert" className={`flex items-start gap-3 rounded-lg border p-4 ${variantStyles[variant]}`}>
      <div className="flex-1">
        {title && <p className="mb-1 text-sm font-semibold">{title}</p>}
        <p className="text-sm">{children}</p>
      </div>
      <button
        type="button"
        onClick={handleDismiss}
        aria-label="Dismiss alert"
        className="shrink-0 rounded-md p-1 opacity-60 hover:opacity-100"
      >
        <svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
          <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
        </svg>
      </button>
    </div>
  );
}

Manages its own visibility with internal state while also exposing an onDismiss callback for the parent to react (e.g., persist the dismissal). The close button uses aria-label since it has no visible text. Requires "use client" for state.

type AlertVariant = "info" | "success" | "warning" | "error";
 
interface AlertProps {
  variant?: AlertVariant;
  children: React.ReactNode;
  actionLabel: string;
  actionHref: string;
}
 
const variantStyles: Record<AlertVariant, { container: string; link: string }> = {
  info: { container: "border-blue-200 bg-blue-50 text-blue-800", link: "text-blue-700 hover:text-blue-900" },
  success: { container: "border-green-200 bg-green-50 text-green-800", link: "text-green-700 hover:text-green-900" },
  warning: { container: "border-yellow-200 bg-yellow-50 text-yellow-800", link: "text-yellow-700 hover:text-yellow-900" },
  error: { container: "border-red-200 bg-red-50 text-red-800", link: "text-red-700 hover:text-red-900" },
};
 
export function AlertWithAction({ variant = "info", children, actionLabel, actionHref }: AlertProps) {
  const styles = variantStyles[variant];
 
  return (
    <div role="alert" className={`flex items-center justify-between gap-4 rounded-lg border p-4 ${styles.container}`}>
      <p className="text-sm">{children}</p>
      <a
        href={actionHref}
        className={`shrink-0 text-sm font-semibold underline ${styles.link}`}
      >
        {actionLabel}
      </a>
    </div>
  );
}
 
// Usage
<AlertWithAction variant="warning" actionLabel="Upgrade plan" actionHref="/billing">
  You have used 90% of your monthly API quota.
</AlertWithAction>

The action link sits at the right edge of the alert using justify-between, giving it visual prominence without breaking the message flow. The shrink-0 on the link prevents it from wrapping when the message text is long.

Complex Implementation

"use client";
 
import { forwardRef, useState, useEffect, useCallback, type ReactNode } from "react";
 
type AlertVariant = "info" | "success" | "warning" | "error";
 
interface AlertAction {
  label: string;
  onClick: () => void;
}
 
interface AlertProps {
  variant?: AlertVariant;
  title?: string;
  children: ReactNode;
  icon?: ReactNode;
  dismissible?: boolean;
  onDismiss?: () => void;
  autoClose?: number;
  action?: AlertAction;
  className?: string;
}
 
const variantConfig: Record<
  AlertVariant,
  { container: string; icon: ReactNode; iconColor: string }
> = {
  info: {
    container: "border-blue-200 bg-blue-50 text-blue-800",
    iconColor: "text-blue-500",
    icon: (
      <svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
        <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
      </svg>
    ),
  },
  success: {
    container: "border-green-200 bg-green-50 text-green-800",
    iconColor: "text-green-500",
    icon: (
      <svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
        <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
      </svg>
    ),
  },
  warning: {
    container: "border-yellow-200 bg-yellow-50 text-yellow-800",
    iconColor: "text-yellow-500",
    icon: (
      <svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
        <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L4.082 16.5c-.77.833.192 2.5 1.732 2.5z" />
      </svg>
    ),
  },
  error: {
    container: "border-red-200 bg-red-50 text-red-800",
    iconColor: "text-red-500",
    icon: (
      <svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
        <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
      </svg>
    ),
  },
};
 
export const Alert = forwardRef<HTMLDivElement, AlertProps>(function Alert(
  {
    variant = "info",
    title,
    children,
    icon,
    dismissible = false,
    onDismiss,
    autoClose,
    action,
    className,
  },
  ref
) {
  const [visible, setVisible] = useState(true);
  const [exiting, setExiting] = useState(false);
  const config = variantConfig[variant];
 
  const dismiss = useCallback(() => {
    setExiting(true);
    const timer = setTimeout(() => {
      setVisible(false);
      onDismiss?.();
    }, 200);
    return () => clearTimeout(timer);
  }, [onDismiss]);
 
  useEffect(() => {
    if (!autoClose) return;
    const timer = setTimeout(dismiss, autoClose);
    return () => clearTimeout(timer);
  }, [autoClose, dismiss]);
 
  if (!visible) return null;
 
  const displayIcon = icon ?? config.icon;
 
  return (
    <div
      ref={ref}
      role="alert"
      aria-live={variant === "error" ? "assertive" : "polite"}
      className={[
        "flex items-start gap-3 rounded-lg border p-4 transition-opacity duration-200",
        config.container,
        exiting ? "opacity-0" : "opacity-100",
        className ?? "",
      ].join(" ")}
    >
      <span className={`shrink-0 pt-px ${config.iconColor}`}>{displayIcon}</span>
 
      <div className="flex-1">
        {title && (
          <p className="mb-1 text-sm font-semibold leading-5">{title}</p>
        )}
        <div className="text-sm leading-5">{children}</div>
        {action && (
          <button
            type="button"
            onClick={action.onClick}
            className="mt-2 text-sm font-semibold underline underline-offset-2 hover:no-underline"
          >
            {action.label}
          </button>
        )}
      </div>
 
      {dismissible && (
        <button
          type="button"
          onClick={dismiss}
          aria-label="Dismiss alert"
          className="shrink-0 rounded-md p-1 opacity-60 transition-opacity hover:opacity-100"
        >
          <svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
            <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
          </svg>
        </button>
      )}
    </div>
  );
});
 
// Usage
function SettingsPage() {
  const [saved, setSaved] = useState(false);
 
  return (
    <div className="space-y-4">
      {saved && (
        <Alert
          variant="success"
          title="Settings saved"
          dismissible
          autoClose={5000}
          onDismiss={() => setSaved(false)}
        >
          Your preferences have been updated successfully.
        </Alert>
      )}
 
      <Alert
        variant="warning"
        title="Browser support"
        action={{ label: "Learn more", onClick: () => console.log("clicked") }}
      >
        Some features may not work in Internet Explorer 11.
      </Alert>
 
      <Alert variant="error" title="Connection failed" dismissible>
        Unable to reach the server. Check your network and try again.
      </Alert>
    </div>
  );
}

Key aspects:

  • Auto-close with cleanup -- the autoClose prop accepts a duration in milliseconds and automatically dismisses the alert. The timeout is cleared on unmount to prevent state updates on removed components.
  • Exit animation -- dismissal triggers a 200ms opacity fade by setting exiting state before removing the element, giving a polished feel without requiring an animation library.
  • Adaptive aria-live -- error alerts use aria-live="assertive" to interrupt the screen reader immediately, while other variants use "polite" to wait for a natural pause in speech.
  • Icon override -- each variant provides a default icon, but the icon prop lets consumers substitute their own SVG or icon component without changing the layout structure.
  • Action button integration -- the optional action prop places an inline button below the message, keeping the alert self-contained. This avoids the need for consumers to compose custom content for common call-to-action patterns.
  • forwardRef -- allows parent components to measure the alert height for smooth enter/exit animations or to scroll it into view after dynamic insertion.

Gotchas

  • role="alert" announces on every re-render -- if the alert text changes due to a parent re-render, the screen reader re-announces the entire content. Avoid placing alerts inside components that re-render frequently, or memoize the alert.

  • Color alone conveying severity -- red for error and green for success is meaningless to colorblind users. Always pair the color with an icon and descriptive text to communicate the alert type.

  • Auto-close on error alerts frustrates users -- error messages that disappear before the user has time to read and act on them cause confusion. Avoid autoClose on error and warning variants.

  • Dismissing removes the element from the DOM -- after dismissal, the alert is gone and cannot be restored without the parent re-mounting it. If you need "undo dismiss", keep the alert in the DOM but visually hidden, or manage state in the parent.

  • Multiple alerts stacking without spacing -- rendering several alerts in a row without a flex/gap or margin utility creates a visually cramped layout. Wrap alerts in a space-y-3 container.

  • aria-live region not detected on initial render -- screen readers only announce changes to an aria-live region, not its initial content. If the alert is present on first paint, the user will not hear it unless they navigate to it manually.

  • Toast -- Toasts are transient floating messages; alerts are persistent inline banners
  • Badge -- Badges indicate status on individual items; alerts communicate page-level messages
  • Modal -- Modals block interaction for critical confirmations; alerts inform without blocking
  • Button -- Alerts with actions often contain buttons for user response
  • Input -- Form validation errors pair well with error alerts at the top of the form