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.
With Action Link
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
autoCloseprop 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
exitingstate 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
iconprop lets consumers substitute their own SVG or icon component without changing the layout structure. - Action button integration -- the optional
actionprop 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
autoCloseon 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-3container. -
aria-liveregion not detected on initial render -- screen readers only announce changes to anaria-liveregion, not its initial content. If the alert is present on first paint, the user will not hear it unless they navigate to it manually.
Related
- 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