Alert
An inline banner component that communicates status messages, warnings, or contextual information to the user within the normal page flow.
Search across all documentation pages
An inline banner component that communicates status messages, warnings, or contextual information to the user within the normal page flow.
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
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.
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.
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.
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.
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.
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.
"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.
"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:
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.exiting state before removing the element, giving a polished feel without requiring an animation library.aria-live="assertive" to interrupt the screen reader immediately, while other variants use "polite" to wait for a natural pause in speech.icon prop lets consumers substitute their own SVG or icon component without changing the layout structure.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.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.
Reviewed by Chris St. John·Last updated Jul 10, 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥