Toast Slide Alert
Original · freeA toast that slides in with an icon and auto-dismiss progress.
Copy prompt gives Claude Code, Cursor or v0 a ready-to-paste prompt that recreates this exact component.
npx shadcn@latest add https://webinnoventix.com/r/alert-toast-slide.json"use client";
import { useCallback, useEffect, useRef, useState, type ReactNode } from "react";
import {
AnimatePresence,
motion,
useReducedMotion,
type Variants,
} from "motion/react";
type ToastTone = "success" | "error" | "info" | "warning";
type ToastItem = {
id: number;
tone: ToastTone;
title: string;
message: string;
duration: number;
};
type ToneStyle = {
ring: string;
iconWrap: string;
bar: string;
icon: ReactNode;
label: string;
};
const TONES: Record<ToastTone, ToneStyle> = {
success: {
ring: "ring-emerald-200 dark:ring-emerald-500/30",
iconWrap:
"bg-emerald-100 text-emerald-700 dark:bg-emerald-500/15 dark:text-emerald-300",
bar: "bg-emerald-500 dark:bg-emerald-400",
label: "Success",
icon: (
<svg viewBox="0 0 20 20" fill="none" className="h-5 w-5" aria-hidden="true">
<path
d="M4 10.5 8.2 14.5 16 6"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
),
},
error: {
ring: "ring-rose-200 dark:ring-rose-500/30",
iconWrap:
"bg-rose-100 text-rose-700 dark:bg-rose-500/15 dark:text-rose-300",
bar: "bg-rose-500 dark:bg-rose-400",
label: "Error",
icon: (
<svg viewBox="0 0 20 20" fill="none" className="h-5 w-5" aria-hidden="true">
<path
d="M6.5 6.5 13.5 13.5M13.5 6.5 6.5 13.5"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
),
},
info: {
ring: "ring-sky-200 dark:ring-sky-500/30",
iconWrap:
"bg-sky-100 text-sky-700 dark:bg-sky-500/15 dark:text-sky-300",
bar: "bg-sky-500 dark:bg-sky-400",
label: "Info",
icon: (
<svg viewBox="0 0 20 20" fill="none" className="h-5 w-5" aria-hidden="true">
<circle cx="10" cy="10" r="7.5" stroke="currentColor" strokeWidth="1.8" />
<path
d="M10 9v4.5"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
/>
<circle cx="10" cy="6.4" r="1.05" fill="currentColor" />
</svg>
),
},
warning: {
ring: "ring-amber-200 dark:ring-amber-500/30",
iconWrap:
"bg-amber-100 text-amber-700 dark:bg-amber-500/15 dark:text-amber-300",
bar: "bg-amber-500 dark:bg-amber-400",
label: "Warning",
icon: (
<svg viewBox="0 0 20 20" fill="none" className="h-5 w-5" aria-hidden="true">
<path
d="M10 2.8 18 16.2H2L10 2.8Z"
stroke="currentColor"
strokeWidth="1.8"
strokeLinejoin="round"
/>
<path
d="M10 8v3.4"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
/>
<circle cx="10" cy="13.7" r="1.05" fill="currentColor" />
</svg>
),
},
};
const PRESETS: Omit<ToastItem, "id">[] = [
{
tone: "success",
title: "Deployment live",
message: "web-innoventix-v3 shipped to production in 42s.",
duration: 5000,
},
{
tone: "info",
title: "New comment",
message: "Salman replied to your thread on the pricing strip.",
duration: 5500,
},
{
tone: "warning",
title: "Storage almost full",
message: "You've used 92% of your media library quota.",
duration: 6000,
},
{
tone: "error",
title: "Payment declined",
message: "Card ending 4417 was rejected. Update billing to retry.",
duration: 6500,
},
];
const BUTTON_TONES: { tone: ToastTone; label: string; classes: string }[] = [
{
tone: "success",
label: "Success",
classes:
"bg-emerald-600 text-white hover:bg-emerald-500 focus-visible:outline-emerald-600 dark:bg-emerald-500 dark:hover:bg-emerald-400",
},
{
tone: "info",
label: "Info",
classes:
"bg-sky-600 text-white hover:bg-sky-500 focus-visible:outline-sky-600 dark:bg-sky-500 dark:hover:bg-sky-400",
},
{
tone: "warning",
label: "Warning",
classes:
"bg-amber-500 text-white hover:bg-amber-400 focus-visible:outline-amber-500 dark:bg-amber-500 dark:hover:bg-amber-400",
},
{
tone: "error",
label: "Error",
classes:
"bg-rose-600 text-white hover:bg-rose-500 focus-visible:outline-rose-600 dark:bg-rose-500 dark:hover:bg-rose-400",
},
];
function Toast({
item,
onDismiss,
reduce,
}: {
item: ToastItem;
onDismiss: (id: number) => void;
reduce: boolean;
}) {
const tone = TONES[item.tone];
const [paused, setPaused] = useState(false);
const [remaining, setRemaining] = useState(item.duration);
const startRef = useRef<number>(Date.now());
const leftRef = useRef<number>(item.duration);
const rafRef = useRef<number | null>(null);
useEffect(() => {
if (paused) {
leftRef.current = remaining;
return;
}
startRef.current = Date.now();
const tick = () => {
const elapsed = Date.now() - startRef.current;
const next = Math.max(0, leftRef.current - elapsed);
setRemaining(next);
if (next <= 0) {
onDismiss(item.id);
return;
}
rafRef.current = requestAnimationFrame(tick);
};
rafRef.current = requestAnimationFrame(tick);
return () => {
if (rafRef.current !== null) cancelAnimationFrame(rafRef.current);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [paused, item.id]);
const pct = Math.max(0, Math.min(100, (remaining / item.duration) * 100));
const variants: Variants = {
hidden: { opacity: 0, x: reduce ? 0 : 48, scale: reduce ? 1 : 0.96 },
show: {
opacity: 1,
x: 0,
scale: 1,
transition: { type: "spring", stiffness: 420, damping: 32, mass: 0.7 },
},
exit: {
opacity: 0,
x: reduce ? 0 : 48,
scale: reduce ? 1 : 0.96,
transition: { duration: 0.2, ease: "easeIn" },
},
};
return (
<motion.div
layout={!reduce}
variants={variants}
initial="hidden"
animate="show"
exit="exit"
role={item.tone === "error" ? "alert" : "status"}
aria-live={item.tone === "error" ? "assertive" : "polite"}
onMouseEnter={() => setPaused(true)}
onMouseLeave={() => setPaused(false)}
onFocusCapture={() => setPaused(true)}
onBlurCapture={() => setPaused(false)}
className={[
"pointer-events-auto relative w-full max-w-sm overflow-hidden rounded-xl",
"bg-white/95 ring-1 shadow-lg shadow-slate-900/5 backdrop-blur",
"dark:bg-slate-900/95 dark:shadow-black/30",
tone.ring,
].join(" ")}
>
<div className="flex items-start gap-3 p-4">
<span
className={[
"mt-0.5 flex h-8 w-8 shrink-0 items-center justify-center rounded-full",
tone.iconWrap,
].join(" ")}
>
{tone.icon}
</span>
<div className="min-w-0 flex-1">
<p className="text-sm font-semibold text-slate-900 dark:text-slate-50">
{item.title}
</p>
<p className="mt-0.5 text-sm leading-snug text-slate-600 dark:text-slate-300">
{item.message}
</p>
</div>
<button
type="button"
onClick={() => onDismiss(item.id)}
aria-label={`Dismiss ${tone.label.toLowerCase()} notification`}
className={[
"-mr-1 -mt-1 flex h-7 w-7 shrink-0 items-center justify-center rounded-md",
"text-slate-400 transition hover:bg-slate-100 hover:text-slate-700",
"focus:outline-none focus-visible:ring-2 focus-visible:ring-slate-400",
"dark:text-slate-500 dark:hover:bg-slate-800 dark:hover:text-slate-200",
"dark:focus-visible:ring-slate-500",
].join(" ")}
>
<svg viewBox="0 0 20 20" fill="none" className="h-4 w-4" aria-hidden="true">
<path
d="M5.5 5.5 14.5 14.5M14.5 5.5 5.5 14.5"
stroke="currentColor"
strokeWidth="1.8"
strokeLinecap="round"
/>
</svg>
</button>
</div>
<div
className="h-1 w-full bg-slate-100 dark:bg-slate-800"
role="presentation"
>
<div
className={["h-full rounded-r-full", tone.bar].join(" ")}
style={{
width: `${pct}%`,
transition: paused ? "none" : "width 90ms linear",
}}
/>
</div>
</motion.div>
);
}
export default function AlertToastSlide() {
const reduce = useReducedMotion() ?? false;
const [toasts, setToasts] = useState<ToastItem[]>([]);
const idRef = useRef(0);
const cycleRef = useRef(0);
const dismiss = useCallback((id: number) => {
setToasts((prev) => prev.filter((t) => t.id !== id));
}, []);
const push = useCallback(
(tone?: ToastTone) => {
const preset =
tone !== undefined
? (PRESETS.find((p) => p.tone === tone) ?? PRESETS[0])
: PRESETS[cycleRef.current++ % PRESETS.length];
idRef.current += 1;
const next: ToastItem = { ...preset, id: idRef.current };
setToasts((prev) => [next, ...prev].slice(0, 4));
},
[],
);
const clearAll = useCallback(() => setToasts([]), []);
return (
<section className="relative w-full bg-slate-50 px-4 py-20 sm:py-24 dark:bg-slate-950">
<style>{`
@keyframes ats_pulse_ring {
0%, 100% { opacity: 0.55; transform: scale(1); }
50% { opacity: 0.15; transform: scale(1.6); }
}
@media (prefers-reduced-motion: reduce) {
.ats_pulse { animation: none !important; }
}
`}</style>
<div className="mx-auto max-w-xl">
<div className="text-center">
<span className="inline-flex items-center gap-2 rounded-full bg-indigo-50 px-3 py-1 text-xs font-medium text-indigo-700 ring-1 ring-indigo-100 dark:bg-indigo-500/10 dark:text-indigo-300 dark:ring-indigo-500/20">
<span className="relative flex h-2 w-2">
<span className="ats_pulse absolute inline-flex h-full w-full rounded-full bg-indigo-500" style={{ animation: "ats_pulse_ring 2.4s ease-in-out infinite" }} />
<span className="relative inline-flex h-2 w-2 rounded-full bg-indigo-500" />
</span>
Toast notifications
</span>
<h2 className="mt-4 text-2xl font-semibold tracking-tight text-slate-900 sm:text-3xl dark:text-slate-50">
Slide-in toast with auto-dismiss
</h2>
<p className="mx-auto mt-3 max-w-md text-sm text-slate-600 dark:text-slate-400">
Trigger a notification. Each toast slides in, counts down its own
progress bar, and pauses whenever you hover or focus it.
</p>
</div>
<div className="mt-8 flex flex-wrap items-center justify-center gap-2.5">
{BUTTON_TONES.map((b) => (
<button
key={b.tone}
type="button"
onClick={() => push(b.tone)}
className={[
"rounded-lg px-3.5 py-2 text-sm font-medium shadow-sm transition",
"focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2",
"active:scale-[0.97]",
b.classes,
].join(" ")}
>
{b.label}
</button>
))}
<button
type="button"
onClick={clearAll}
disabled={toasts.length === 0}
className={[
"rounded-lg px-3.5 py-2 text-sm font-medium transition",
"border border-slate-300 text-slate-700 hover:bg-slate-100",
"focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-slate-400",
"disabled:cursor-not-allowed disabled:opacity-40",
"dark:border-slate-700 dark:text-slate-200 dark:hover:bg-slate-800",
].join(" ")}
>
Clear all
</button>
</div>
<p className="mt-4 text-center text-xs text-slate-400 dark:text-slate-500">
{toasts.length === 0
? "No active notifications"
: `${toasts.length} active ${toasts.length === 1 ? "toast" : "toasts"} · newest on top`}
</p>
</div>
<div
aria-live="polite"
className="pointer-events-none fixed inset-x-0 bottom-4 z-50 flex flex-col items-center gap-3 px-4 sm:inset-x-auto sm:right-6 sm:bottom-6 sm:items-end"
>
<AnimatePresence mode="popLayout" initial={false}>
{toasts.map((t) => (
<Toast key={t.id} item={t} onDismiss={dismiss} reduce={reduce} />
))}
</AnimatePresence>
</div>
</section>
);
}Dependencies
Licence
Built by Web Innoventix. Free for personal and commercial use, no attribution required.
Built by Web Innoventix
Need a custom interface, a full website, or SEO that gets you cited by AI? We design, build and rank it end to end.
Get a free quoteSimilar components
Browse all →
Soft Variants Alert
OriginalSoft-tinted alerts: success, info, warning and error, each with an icon.

Solid Variants Alert
OriginalSolid-colour alerts across the four states.

Outline Variants Alert
OriginalOutlined alerts across the four states.

Left Accent Alert
OriginalAlerts with a coloured left accent bar and icon.

Dismissible Alert
OriginalDismissible alerts with a working close button.

With Action Alert
OriginalAn alert with title, body and action buttons.
Icon Large Alert
OriginalAn alert led by a large circular icon.

Banner Top Alert
OriginalA full-width top announcement banner, dismissible.

Toast Stack Alert
OriginalA stack of dismissible toasts in a corner.

Progress Alert
OriginalAn alert with a countdown progress bar.

Gradient Alert
OriginalA gradient promo banner alert with a call to action.

Inline Form Alert
OriginalAn alert containing an inline email input and button.

