Alerta
Un componente de banner inline que comunica mensajes de estado, advertencias o información contextual al usuario dentro del flujo normal de la página.
Casos de Uso
- Mostrar un mensaje de éxito después del envío de un formulario
- Advertir a los usuarios sobre cambios sin guardar antes de navegar
- Mostrar un resumen de errores en la parte superior de un formulario con fallos de validación
- Informar a los usuarios sobre mantenimiento programado o tiempo de inactividad
- Presentar un consejo o nota informativa dentro del contenido de documentación
- Alertar a los usuarios sobre problemas a nivel de cuenta (facturación, verificación)
- Mostrar un aviso de obsolescencia para una API o característica
Implementación Más Simple
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>
);
}Una alerta informativa estática con role="alert" para que los lectores de pantalla la anuncien inmediatamente cuando aparezca en el DOM. No se necesita "use client" ya que no hay interactividad.
Variaciones
Alerta de Información
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>
);
}La variante de información usa tonos azules para indicar contenido neutral e informativo. La prop opcional title agrega una línea de encabezado en negrita arriba de la descripción para mensajería de dos niveles.
Alerta de Éxito
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>
);
}El verde señala un resultado positivo -- formulario guardado, pago procesado, cuenta verificada. Esta variante generalmente se muestra después de una acción exitosa y puede emparejarse con un botón de cierre descartable.
Alerta de Advertencia
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>
);
}El amarillo llama la atención sin implicar fracaso. Usa esto para condiciones que necesitan reconocimiento pero no son bloqueantes -- espacio en disco bajo, límites de velocidad que se aproximan, o características obsoletas aún en uso.
Alerta Destructiva / de Error
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>
);
}El rojo comunica problemas críticos que requieren atención inmediata -- errores de validación, solicitudes fallidas, o confirmaciones de acciones destructivas. Considera usar aria-live="assertive" para errores que aparecen dinámicamente.
Con Icono
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>
);
}Los iconos refuerzan el significado de la alerta más allá del color solo, lo cual es esencial para la accesibilidad. El shrink-0 en el contenedor del icono evita que se comprima cuando el contenido de texto es largo. El diseño flex con gap-3 mantiene espaciado consistente entre el icono y el texto.
Descartable con Botón de Cierre
"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>
);
}Gestiona su propia visibilidad con estado interno mientras también expone un callback onDismiss para que el componente padre reaccione (p. ej., persistir el descarte). El botón de cierre usa aria-label ya que no tiene texto visible. Requiere "use client" para state.
Con Enlace de Acción
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>
);
}
// Uso
<AlertWithAction variant="warning" actionLabel="Actualizar plan" actionHref="/billing">
Has utilizado el 90% de tu cuota mensual de API.
</AlertWithAction>El enlace de acción se sitúa en el borde derecho de la alerta usando justify-between, dándole prominencia visual sin romper el flujo del mensaje. El shrink-0 en el enlace evita que se ajuste cuando el texto del mensaje es largo.
Implementación Compleja
"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>
);
});
// Uso
function SettingsPage() {
const [saved, setSaved] = useState(false);
return (
<div className="space-y-4">
{saved && (
<Alert
variant="success"
title="Configuración guardada"
dismissible
autoClose={5000}
onDismiss={() => setSaved(false)}
>
Tus preferencias han sido actualizadas exitosamente.
</Alert>
)}
<Alert
variant="warning"
title="Compatibilidad del navegador"
action={{ label: "Saber más", onClick: () => console.log("clicked") }}
>
Algunas características podrían no funcionar en Internet Explorer 11.
</Alert>
<Alert variant="error" title="Conexión fallida" dismissible>
No se puede alcanzar el servidor. Verifica tu red e intenta de nuevo.
</Alert>
</div>
);
}Aspectos clave:
- Cierre automático con limpieza -- la prop
autoCloseacepta una duración en milisegundos y descarta automáticamente la alerta. El timeout se limpia al desmontar para evitar actualizaciones de estado en componentes removidos. - Animación de salida -- el descarte desencadena un desvanecimiento de opacidad de 200ms estableciendo el estado
exitingantes de remover el elemento, dando una sensación pulida sin requerir una librería de animación. - aria-live adaptativo -- las alertas de error usan
aria-live="assertive"para interrumpir el lector de pantalla inmediatamente, mientras que otras variantes usan"polite"para esperar una pausa natural en el habla. - Anulación de icono -- cada variante proporciona un icono predeterminado, pero la prop
iconpermite a los consumidores sustituir su propio SVG o componente de icono sin cambiar la estructura del diseño. - Integración de botón de acción -- la prop
actionopcional coloca un botón inline debajo del mensaje, manteniendo la alerta autocontenida. Esto evita la necesidad de que los consumidores compongan contenido personalizado para patrones comunes de llamada a la acción. - forwardRef -- permite que los componentes padres midan la altura de la alerta para animaciones suave de entrada/salida o para desplazarla a la vista después de la inserción dinámica.
Inconvenientes
-
role="alert"anuncia en cada re-renderizado -- si el texto de la alerta cambia debido a un re-renderizado del componente padre, el lector de pantalla re-anuncia todo el contenido. Evita colocar alertas dentro de componentes que se re-renderizar frecuentemente, o memoiza la alerta. -
El color solo comunicando severidad -- rojo para error y verde para éxito no tiene sentido para usuarios daltónicos. Siempre empareja el color con un icono y texto descriptivo para comunicar el tipo de alerta.
-
El cierre automático en alertas de error frustra a los usuarios -- los mensajes de error que desaparecen antes de que el usuario tenga tiempo de leerlos y actuar sobre ellos causa confusión. Evita
autoCloseen variantes de error y advertencia. -
El descarte remueve el elemento del DOM -- después del descarte, la alerta se ha ido y no puede ser restaurada sin que el componente padre la remonte. Si necesitas "deshacer descarte", mantén la alerta en el DOM pero visualmente oculta, o gestiona el estado en el componente padre.
-
Múltiples alertas apiladas sin espaciado -- renderizar varias alertas seguidas sin una utilidad flex/gap o margin crea un diseño visualmente abarrotado. Envuelve las alertas en un contenedor
space-y-3. -
La región
aria-liveno se detecta en el renderizado inicial -- los lectores de pantalla solo anuncian cambios a una regiónaria-live, no su contenido inicial. Si la alerta está presente en la primera pintura, el usuario no la escuchará a menos que navegue a ella manualmente.
Relacionado
- Toast -- Los toasts son mensajes flotantes transitorios; las alertas son banners inline persistentes
- Badge -- Los badges indican estado en elementos individuales; las alertas comunican mensajes a nivel de página
- Modal -- Los modales bloquean la interacción para confirmaciones críticas; las alertas informan sin bloquear
- Button -- Las alertas con acciones a menudo contienen botones para respuesta del usuario
- Input -- Los errores de validación de formularios funcionan bien con alertas de error en la parte superior del formulario