Expandable Alert
Original · freeAn alert that expands to reveal more detail.
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-expandable.json"use client";
import { type ReactNode, useId, useRef, useState } from "react";
type Tone = "info" | "success" | "warning" | "danger";
interface AlertItem {
id: string;
tone: Tone;
title: string;
summary: string;
detail: ReactNode;
meta: string;
}
const toneStyles: Record<
Tone,
{
ring: string;
bg: string;
iconWrap: string;
iconColor: string;
title: string;
accent: string;
chip: string;
icon: ReactNode;
}
> = {
info: {
ring: "ring-sky-200/70 dark:ring-sky-500/25",
bg: "bg-sky-50/80 dark:bg-sky-950/40",
iconWrap: "bg-sky-100 dark:bg-sky-500/15",
iconColor: "text-sky-600 dark:text-sky-300",
title: "text-sky-950 dark:text-sky-100",
accent: "bg-sky-500",
chip: "bg-sky-100 text-sky-700 dark:bg-sky-500/15 dark:text-sky-200",
icon: (
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M11.25 11.25h.75v4.5m-.75 0h1.5M12 8.25h.008v.008H12V8.25Z"
/>
),
},
success: {
ring: "ring-emerald-200/70 dark:ring-emerald-500/25",
bg: "bg-emerald-50/80 dark:bg-emerald-950/40",
iconWrap: "bg-emerald-100 dark:bg-emerald-500/15",
iconColor: "text-emerald-600 dark:text-emerald-300",
title: "text-emerald-950 dark:text-emerald-100",
accent: "bg-emerald-500",
chip: "bg-emerald-100 text-emerald-700 dark:bg-emerald-500/15 dark:text-emerald-200",
icon: (
<path
strokeLinecap="round"
strokeLinejoin="round"
d="m4.5 12.75 6 6 9-13.5"
/>
),
},
warning: {
ring: "ring-amber-200/70 dark:ring-amber-500/25",
bg: "bg-amber-50/80 dark:bg-amber-950/40",
iconWrap: "bg-amber-100 dark:bg-amber-500/15",
iconColor: "text-amber-600 dark:text-amber-300",
title: "text-amber-950 dark:text-amber-100",
accent: "bg-amber-500",
chip: "bg-amber-100 text-amber-800 dark:bg-amber-500/15 dark:text-amber-200",
icon: (
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M12 9v3.75m0 3.75h.008M10.29 3.86 1.82 18a1.5 1.5 0 0 0 1.29 2.25h17.78A1.5 1.5 0 0 0 22.18 18L13.71 3.86a1.5 1.5 0 0 0-2.58 0Z"
/>
),
},
danger: {
ring: "ring-rose-200/70 dark:ring-rose-500/25",
bg: "bg-rose-50/80 dark:bg-rose-950/40",
iconWrap: "bg-rose-100 dark:bg-rose-500/15",
iconColor: "text-rose-600 dark:text-rose-300",
title: "text-rose-950 dark:text-rose-100",
accent: "bg-rose-500",
chip: "bg-rose-100 text-rose-700 dark:bg-rose-500/15 dark:text-rose-200",
icon: (
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M12 9v3.75m9-.75a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9 3.75h.008v.008H12v-.008Z"
/>
),
},
};
const alerts: AlertItem[] = [
{
id: "deploy",
tone: "success",
title: "Deploy #1482 shipped to production",
summary: "All 214 checks passed. Rollout completed in 3m 12s.",
meta: "2 min ago",
detail: (
<>
<p>
The <code className="rounded bg-black/5 px-1 py-0.5 font-mono text-[0.8em] dark:bg-white/10">web-api</code>{" "}
and <code className="rounded bg-black/5 px-1 py-0.5 font-mono text-[0.8em] dark:bg-white/10">edge-cache</code>{" "}
services were rolled out to all three regions with zero failed health checks.
</p>
<ul className="mt-3 space-y-1.5">
<li>us-east-1 · 6 pods healthy</li>
<li>eu-west-2 · 4 pods healthy</li>
<li>ap-south-1 · 4 pods healthy</li>
</ul>
</>
),
},
{
id: "usage",
tone: "warning",
title: "You've used 84% of your monthly API quota",
summary: "At the current pace you'll hit the limit around July 26.",
meta: "18 min ago",
detail: (
<>
<p>
You have <strong>160,000</strong> of 1,000,000 requests remaining this cycle. Requests over the
limit are throttled to 10/second rather than rejected.
</p>
<p className="mt-3">
Upgrading to the Scale plan raises the ceiling to 5M requests and removes the throttle
entirely. The change takes effect immediately and is prorated.
</p>
</>
),
},
{
id: "payment",
tone: "danger",
title: "We couldn't process your last payment",
summary: "The card ending in 4291 was declined by the issuer.",
meta: "1 hour ago",
detail: (
<>
<p>
Your subscription stays active until <strong>July 21</strong>. If the payment still fails
after two more attempts, workspaces on this account will switch to read-only.
</p>
<p className="mt-3">
The most common cause is an expired card or an issuer fraud hold. Updating your billing
details will trigger an immediate retry.
</p>
</>
),
},
];
function Chevron({ open }: { open: boolean }) {
return (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2}
aria-hidden="true"
className={`h-5 w-5 shrink-0 transition-transform duration-300 ease-out ${
open ? "rotate-180" : "rotate-0"
}`}
>
<path strokeLinecap="round" strokeLinejoin="round" d="m19.5 8.25-7.5 7.5-7.5-7.5" />
</svg>
);
}
function ExpandableAlert({ item }: { item: AlertItem }) {
const [open, setOpen] = useState(false);
const [dismissed, setDismissed] = useState(false);
const regionId = useId();
const s = toneStyles[item.tone];
const bodyRef = useRef<HTMLDivElement>(null);
if (dismissed) return null;
const actionLabel =
item.tone === "danger"
? "Update billing"
: item.tone === "warning"
? "Upgrade plan"
: "View details";
return (
<div
role="alert"
className={`relative overflow-hidden rounded-2xl ring-1 ${s.ring} ${s.bg} shadow-sm backdrop-blur transition-shadow duration-300 hover:shadow-md`}
>
<span className={`absolute inset-y-0 left-0 w-1 ${s.accent}`} aria-hidden="true" />
<button
type="button"
onClick={() => setOpen((v) => !v)}
aria-expanded={open}
aria-controls={regionId}
className="group flex w-full items-start gap-3.5 rounded-2xl px-4 py-4 text-left outline-none transition-colors focus-visible:ring-2 focus-visible:ring-slate-900/40 dark:focus-visible:ring-white/40 sm:px-5"
>
<span
className={`mt-0.5 grid h-9 w-9 shrink-0 place-items-center rounded-full ${s.iconWrap} ${s.iconColor}`}
>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.9} className="h-5 w-5">
{s.icon}
</svg>
</span>
<span className="min-w-0 flex-1">
<span className="flex items-center gap-2">
<span className={`truncate text-sm font-semibold sm:text-[0.95rem] ${s.title}`}>
{item.title}
</span>
</span>
<span className="mt-0.5 block text-sm text-slate-600 dark:text-slate-300/90">
{item.summary}
</span>
<span className="mt-2 flex items-center gap-2">
<span
className={`inline-flex items-center rounded-full px-2 py-0.5 text-[0.7rem] font-medium ${s.chip}`}
>
{item.meta}
</span>
<span className="text-[0.7rem] font-medium text-slate-500 transition-colors group-hover:text-slate-700 dark:text-slate-400 dark:group-hover:text-slate-200">
{open ? "Hide details" : "Show details"}
</span>
</span>
</span>
<span className={`mt-1 ${s.iconColor}`}>
<Chevron open={open} />
</span>
</button>
<div
id={regionId}
role="region"
className="grid transition-[grid-template-rows] duration-300 ease-out"
style={{ gridTemplateRows: open ? "1fr" : "0fr" }}
>
<div className="overflow-hidden">
<div
ref={bodyRef}
className={`aex-body px-4 pb-4 pl-16 pr-5 text-sm leading-relaxed text-slate-700 dark:text-slate-300 sm:pl-[4.6rem] ${
open ? "aex-body-in" : ""
}`}
>
<div className="rounded-xl border border-black/5 bg-white/60 p-4 dark:border-white/10 dark:bg-white/5">
{item.detail}
</div>
<div className="mt-3 flex flex-wrap items-center gap-2.5">
<button
type="button"
onClick={() => setDismissed(true)}
className={`inline-flex items-center gap-1.5 rounded-lg px-3.5 py-2 text-xs font-semibold text-white shadow-sm outline-none transition-transform duration-150 hover:brightness-110 focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-offset-transparent active:scale-[0.97] ${s.accent}`}
>
{actionLabel}
</button>
<button
type="button"
onClick={() => setDismissed(true)}
className="inline-flex items-center rounded-lg px-3 py-2 text-xs font-semibold text-slate-600 outline-none transition-colors hover:bg-black/5 focus-visible:ring-2 focus-visible:ring-slate-900/30 dark:text-slate-300 dark:hover:bg-white/10 dark:focus-visible:ring-white/30"
>
Dismiss
</button>
</div>
</div>
</div>
</div>
</div>
);
}
export default function AlertExpandable() {
return (
<section className="relative w-full bg-slate-50 px-4 py-16 dark:bg-slate-950 sm:px-6 sm:py-20">
<style>{`
@keyframes aex-rise {
from { opacity: 0; transform: translateY(6px); }
to { opacity: 1; transform: translateY(0); }
}
.aex-body { opacity: 0; }
.aex-body-in { animation: aex-rise 360ms ease-out both; }
@media (prefers-reduced-motion: reduce) {
.aex-body,
.aex-body-in { animation: none !important; opacity: 1 !important; transform: none !important; }
}
`}</style>
<div className="mx-auto max-w-2xl">
<div className="mb-8">
<span className="inline-flex items-center rounded-full bg-indigo-100 px-3 py-1 text-xs font-semibold text-indigo-700 dark:bg-indigo-500/15 dark:text-indigo-200">
Notifications
</span>
<h2 className="mt-3 text-2xl font-bold tracking-tight text-slate-900 dark:text-white sm:text-3xl">
You have 3 alerts to review
</h2>
<p className="mt-2 text-sm text-slate-600 dark:text-slate-400">
Tap any alert to expand the full context, then act on it or dismiss it.
</p>
</div>
<div className="space-y-3.5">
{alerts.map((item) => (
<ExpandableAlert key={item.id} item={item} />
))}
</div>
</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.

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.

