With Action Alert
Original · freeAn alert with title, body and action buttons.
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-with-action.json"use client";
import { useState, useId, useCallback } from "react";
import { motion, AnimatePresence, useReducedMotion } from "motion/react";
type Tone = "info" | "success" | "warning" | "danger";
type AlertItem = {
id: string;
tone: Tone;
title: string;
body: string;
primary: string;
secondary: string;
};
const TONES: Record<
Tone,
{
ring: string;
bar: string;
iconWrap: string;
iconText: string;
title: string;
body: string;
primaryBtn: string;
secondaryBtn: string;
dismiss: string;
surface: string;
}
> = {
info: {
surface: "bg-sky-50/80 dark:bg-sky-950/30",
ring: "ring-sky-200/70 dark:ring-sky-400/20",
bar: "bg-sky-500 dark:bg-sky-400",
iconWrap: "bg-sky-100 dark:bg-sky-500/15",
iconText: "text-sky-600 dark:text-sky-300",
title: "text-sky-950 dark:text-sky-100",
body: "text-sky-800/80 dark:text-sky-200/70",
primaryBtn:
"bg-sky-600 text-white hover:bg-sky-500 focus-visible:ring-sky-500 dark:bg-sky-500 dark:hover:bg-sky-400",
secondaryBtn:
"text-sky-800 hover:bg-sky-100 focus-visible:ring-sky-500 dark:text-sky-200 dark:hover:bg-sky-500/15",
dismiss:
"text-sky-600/70 hover:bg-sky-100 hover:text-sky-800 focus-visible:ring-sky-500 dark:text-sky-300/70 dark:hover:bg-sky-500/15 dark:hover:text-sky-100",
},
success: {
surface: "bg-emerald-50/80 dark:bg-emerald-950/30",
ring: "ring-emerald-200/70 dark:ring-emerald-400/20",
bar: "bg-emerald-500 dark:bg-emerald-400",
iconWrap: "bg-emerald-100 dark:bg-emerald-500/15",
iconText: "text-emerald-600 dark:text-emerald-300",
title: "text-emerald-950 dark:text-emerald-100",
body: "text-emerald-800/80 dark:text-emerald-200/70",
primaryBtn:
"bg-emerald-600 text-white hover:bg-emerald-500 focus-visible:ring-emerald-500 dark:bg-emerald-500 dark:hover:bg-emerald-400",
secondaryBtn:
"text-emerald-800 hover:bg-emerald-100 focus-visible:ring-emerald-500 dark:text-emerald-200 dark:hover:bg-emerald-500/15",
dismiss:
"text-emerald-600/70 hover:bg-emerald-100 hover:text-emerald-800 focus-visible:ring-emerald-500 dark:text-emerald-300/70 dark:hover:bg-emerald-500/15 dark:hover:text-emerald-100",
},
warning: {
surface: "bg-amber-50/80 dark:bg-amber-950/30",
ring: "ring-amber-200/70 dark:ring-amber-400/20",
bar: "bg-amber-500 dark:bg-amber-400",
iconWrap: "bg-amber-100 dark:bg-amber-500/15",
iconText: "text-amber-600 dark:text-amber-300",
title: "text-amber-950 dark:text-amber-100",
body: "text-amber-800/80 dark:text-amber-200/70",
primaryBtn:
"bg-amber-600 text-white hover:bg-amber-500 focus-visible:ring-amber-500 dark:bg-amber-500 dark:text-amber-950 dark:hover:bg-amber-400",
secondaryBtn:
"text-amber-800 hover:bg-amber-100 focus-visible:ring-amber-500 dark:text-amber-200 dark:hover:bg-amber-500/15",
dismiss:
"text-amber-600/70 hover:bg-amber-100 hover:text-amber-800 focus-visible:ring-amber-500 dark:text-amber-300/70 dark:hover:bg-amber-500/15 dark:hover:text-amber-100",
},
danger: {
surface: "bg-rose-50/80 dark:bg-rose-950/30",
ring: "ring-rose-200/70 dark:ring-rose-400/20",
bar: "bg-rose-500 dark:bg-rose-400",
iconWrap: "bg-rose-100 dark:bg-rose-500/15",
iconText: "text-rose-600 dark:text-rose-300",
title: "text-rose-950 dark:text-rose-100",
body: "text-rose-800/80 dark:text-rose-200/70",
primaryBtn:
"bg-rose-600 text-white hover:bg-rose-500 focus-visible:ring-rose-500 dark:bg-rose-500 dark:hover:bg-rose-400",
secondaryBtn:
"text-rose-800 hover:bg-rose-100 focus-visible:ring-rose-500 dark:text-rose-200 dark:hover:bg-rose-500/15",
dismiss:
"text-rose-600/70 hover:bg-rose-100 hover:text-rose-800 focus-visible:ring-rose-500 dark:text-rose-300/70 dark:hover:bg-rose-500/15 dark:hover:text-rose-100",
},
};
function ToneIcon({ tone, className }: { tone: Tone; className?: string }) {
const common = {
viewBox: "0 0 24 24",
fill: "none",
stroke: "currentColor",
strokeWidth: 2,
strokeLinecap: "round" as const,
strokeLinejoin: "round" as const,
className,
"aria-hidden": true,
};
if (tone === "success") {
return (
<svg {...common}>
<path d="M20 6 9 17l-5-5" />
</svg>
);
}
if (tone === "warning") {
return (
<svg {...common}>
<path d="M10.3 3.9 1.8 18a2 2 0 0 0 1.7 3h17a2 2 0 0 0 1.7-3L13.7 3.9a2 2 0 0 0-3.4 0Z" />
<path d="M12 9v4" />
<path d="M12 17h.01" />
</svg>
);
}
if (tone === "danger") {
return (
<svg {...common}>
<circle cx="12" cy="12" r="9" />
<path d="m15 9-6 6" />
<path d="m9 9 6 6" />
</svg>
);
}
return (
<svg {...common}>
<circle cx="12" cy="12" r="9" />
<path d="M12 16v-4" />
<path d="M12 8h.01" />
</svg>
);
}
const INITIAL: AlertItem[] = [
{
id: "storage",
tone: "warning",
title: "You're almost out of storage",
body: "Your workspace is at 94% of its 20 GB limit. New uploads and backups will pause once you hit the cap.",
primary: "Upgrade plan",
secondary: "Manage files",
},
{
id: "deploy",
tone: "success",
title: "Deployment succeeded",
body: "Build #2481 shipped to production in 38s. All 214 checks passed and the release is now live.",
primary: "View release",
secondary: "Roll back",
},
{
id: "billing",
tone: "danger",
title: "Payment failed",
body: "We couldn't charge the card ending in 4471. Update your billing details before Jul 21 to keep your team's access.",
primary: "Update payment",
secondary: "Contact support",
},
{
id: "invite",
tone: "info",
title: "New team invitation",
body: "Priya Nair invited you to the “Growth” workspace as an editor. The invite expires in 7 days.",
primary: "Accept invite",
secondary: "Decline",
},
];
export default function AlertWithAction() {
const reduce = useReducedMotion();
const uid = useId().replace(/[:]/g, "");
const [items, setItems] = useState<AlertItem[]>(INITIAL);
const [status, setStatus] = useState<string>("");
const dismiss = useCallback((id: string, title: string) => {
setItems((prev) => prev.filter((i) => i.id !== id));
setStatus(`Dismissed alert: ${title}`);
}, []);
const act = useCallback((label: string, title: string) => {
setStatus(`${label} — ${title}`);
}, []);
const reset = useCallback(() => {
setItems(INITIAL);
setStatus("Alerts restored");
}, []);
const prefix = `alertaction-${uid}`;
return (
<section className="relative w-full bg-white px-4 py-16 sm:px-6 sm:py-20 dark:bg-neutral-950">
<style>{`
@keyframes ${prefix}-in {
from { opacity: 0; transform: translateY(10px) scale(0.985); }
to { opacity: 1; transform: translateY(0) scale(1); }
}
.${prefix}-item { animation: ${prefix}-in 0.4s cubic-bezier(0.22, 1, 0.36, 1) both; }
@media (prefers-reduced-motion: reduce) {
.${prefix}-item { animation: none; }
}
`}</style>
<div className="mx-auto max-w-2xl">
<header className="mb-8">
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-indigo-600 dark:text-indigo-400">
Notifications
</p>
<h2 className="mt-2 text-2xl font-semibold tracking-tight text-neutral-900 sm:text-3xl dark:text-neutral-50">
Alerts with actions
</h2>
<p className="mt-2 text-sm text-neutral-500 dark:text-neutral-400">
Contextual banners that pair a clear message with the next step. Each can be acted on or
dismissed.
</p>
</header>
<div role="region" aria-label="Active alerts" className="flex flex-col gap-4">
<AnimatePresence initial={false} mode="popLayout">
{items.map((item) => {
const t = TONES[item.tone];
const titleId = `${prefix}-${item.id}-title`;
const bodyId = `${prefix}-${item.id}-body`;
return (
<motion.div
key={item.id}
layout={!reduce}
initial={reduce ? false : { opacity: 0, y: 10, scale: 0.985 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={
reduce
? { opacity: 0 }
: { opacity: 0, x: 24, scale: 0.97, transition: { duration: 0.2 } }
}
transition={{ duration: 0.35, ease: [0.22, 1, 0.36, 1] }}
role={item.tone === "danger" ? "alert" : "status"}
aria-labelledby={titleId}
aria-describedby={bodyId}
className={`relative overflow-hidden rounded-2xl ring-1 ${t.surface} ${t.ring} shadow-sm backdrop-blur-sm`}
>
<span aria-hidden className={`absolute inset-y-0 left-0 w-1 ${t.bar}`} />
<div className="flex gap-3.5 p-4 pl-5 sm:p-5 sm:pl-6">
<span
className={`mt-0.5 flex h-9 w-9 shrink-0 items-center justify-center rounded-full ${t.iconWrap} ${t.iconText}`}
>
<ToneIcon tone={item.tone} className="h-5 w-5" />
</span>
<div className="min-w-0 flex-1">
<div className="flex items-start justify-between gap-3">
<h3 id={titleId} className={`text-sm font-semibold ${t.title}`}>
{item.title}
</h3>
<button
type="button"
onClick={() => dismiss(item.id, item.title)}
aria-label={`Dismiss: ${item.title}`}
className={`-mr-1 -mt-1 flex h-7 w-7 shrink-0 items-center justify-center rounded-lg transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-offset-transparent ${t.dismiss}`}
>
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
className="h-4 w-4"
aria-hidden
>
<path d="M18 6 6 18" />
<path d="m6 6 12 12" />
</svg>
</button>
</div>
<p id={bodyId} className={`mt-1 text-sm leading-relaxed ${t.body}`}>
{item.body}
</p>
<div className="mt-3.5 flex flex-wrap items-center gap-2">
<button
type="button"
onClick={() => act(item.primary, item.title)}
className={`inline-flex items-center rounded-lg px-3 py-1.5 text-xs font-semibold shadow-sm transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-offset-transparent ${t.primaryBtn}`}
>
{item.primary}
</button>
<button
type="button"
onClick={() => act(item.secondary, item.title)}
className={`inline-flex items-center rounded-lg px-3 py-1.5 text-xs font-semibold transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-offset-transparent ${t.secondaryBtn}`}
>
{item.secondary}
</button>
</div>
</div>
</div>
</motion.div>
);
})}
</AnimatePresence>
{items.length === 0 && (
<div
className={`${reduce ? "" : `${prefix}-item`} flex flex-col items-center justify-center rounded-2xl border border-dashed border-neutral-300 px-6 py-12 text-center dark:border-neutral-700`}
>
<span className="flex h-11 w-11 items-center justify-center rounded-full bg-emerald-100 text-emerald-600 dark:bg-emerald-500/15 dark:text-emerald-300">
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
className="h-5 w-5"
aria-hidden
>
<path d="M20 6 9 17l-5-5" />
</svg>
</span>
<p className="mt-3 text-sm font-medium text-neutral-700 dark:text-neutral-200">
You're all caught up
</p>
<p className="mt-1 text-sm text-neutral-500 dark:text-neutral-400">
No alerts need your attention right now.
</p>
<button
type="button"
onClick={reset}
className="mt-4 inline-flex items-center rounded-lg bg-neutral-900 px-3.5 py-2 text-xs font-semibold text-white shadow-sm transition-colors hover:bg-neutral-700 focus:outline-none focus-visible:ring-2 focus-visible:ring-neutral-500 focus-visible:ring-offset-2 focus-visible:ring-offset-transparent dark:bg-white dark:text-neutral-900 dark:hover:bg-neutral-200"
>
Restore alerts
</button>
</div>
)}
</div>
<p aria-live="polite" role="status" className="sr-only">
{status}
</p>
</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.
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.

Toast Slide Alert
OriginalA toast that slides in with an icon and auto-dismiss progress.

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.

