Web InnoventixFreeCode

Dismissible Alert

Original · free

Dismissible alerts with a working close button.

byWeb InnoventixReact + Tailwind
alertdismissiblealerts
Open

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-dismissible.json
alert-dismissible.tsx
"use client";

import { useState } from "react";

type Variant = "info" | "success" | "warning" | "error";

type AlertItem = {
  id: string;
  variant: Variant;
  title: string;
  message: string;
  action?: { label: string; href: string };
};

const INITIAL_ALERTS: AlertItem[] = [
  {
    id: "deploy",
    variant: "success",
    title: "Deployment finished",
    message: "webinnoventix.com is live on production. Propagation may take up to 5 minutes.",
    action: { label: "View site", href: "https://webinnoventix.com" },
  },
  {
    id: "ssl",
    variant: "info",
    title: "New SSL certificate issued",
    message: "Your certificate auto-renews on 12 Oct 2026. No action is required from you.",
  },
  {
    id: "usage",
    variant: "warning",
    title: "Bandwidth at 82% of monthly quota",
    message: "You have used 41 GB of your 50 GB plan. Consider upgrading before the reset on the 1st.",
    action: { label: "Manage plan", href: "https://webinnoventix.com/billing" },
  },
  {
    id: "payment",
    variant: "error",
    title: "Payment method declined",
    message: "We couldn't charge your card ending in 4417. Update it to avoid service interruption.",
    action: { label: "Update card", href: "https://webinnoventix.com/billing" },
  },
];

const VARIANT_STYLES: Record<
  Variant,
  {
    container: string;
    icon: string;
    title: string;
    body: string;
    action: string;
    close: string;
    ring: string;
    accent: string;
  }
> = {
  info: {
    container:
      "border-sky-200 bg-sky-50 dark:border-sky-900/60 dark:bg-sky-950/40",
    icon: "text-sky-600 dark:text-sky-400",
    title: "text-sky-900 dark:text-sky-100",
    body: "text-sky-800/80 dark:text-sky-200/70",
    action:
      "text-sky-700 hover:text-sky-900 dark:text-sky-300 dark:hover:text-sky-100",
    close:
      "text-sky-500 hover:bg-sky-100 hover:text-sky-700 dark:text-sky-400 dark:hover:bg-sky-900/60 dark:hover:text-sky-200",
    ring: "focus-visible:ring-sky-500",
    accent: "bg-sky-500",
  },
  success: {
    container:
      "border-emerald-200 bg-emerald-50 dark:border-emerald-900/60 dark:bg-emerald-950/40",
    icon: "text-emerald-600 dark:text-emerald-400",
    title: "text-emerald-900 dark:text-emerald-100",
    body: "text-emerald-800/80 dark:text-emerald-200/70",
    action:
      "text-emerald-700 hover:text-emerald-900 dark:text-emerald-300 dark:hover:text-emerald-100",
    close:
      "text-emerald-500 hover:bg-emerald-100 hover:text-emerald-700 dark:text-emerald-400 dark:hover:bg-emerald-900/60 dark:hover:text-emerald-200",
    ring: "focus-visible:ring-emerald-500",
    accent: "bg-emerald-500",
  },
  warning: {
    container:
      "border-amber-200 bg-amber-50 dark:border-amber-900/60 dark:bg-amber-950/40",
    icon: "text-amber-600 dark:text-amber-400",
    title: "text-amber-900 dark:text-amber-100",
    body: "text-amber-800/80 dark:text-amber-200/70",
    action:
      "text-amber-700 hover:text-amber-900 dark:text-amber-300 dark:hover:text-amber-100",
    close:
      "text-amber-500 hover:bg-amber-100 hover:text-amber-700 dark:text-amber-400 dark:hover:bg-amber-900/60 dark:hover:text-amber-200",
    ring: "focus-visible:ring-amber-500",
    accent: "bg-amber-500",
  },
  error: {
    container:
      "border-rose-200 bg-rose-50 dark:border-rose-900/60 dark:bg-rose-950/40",
    icon: "text-rose-600 dark:text-rose-400",
    title: "text-rose-900 dark:text-rose-100",
    body: "text-rose-800/80 dark:text-rose-200/70",
    action:
      "text-rose-700 hover:text-rose-900 dark:text-rose-300 dark:hover:text-rose-100",
    close:
      "text-rose-500 hover:bg-rose-100 hover:text-rose-700 dark:text-rose-400 dark:hover:bg-rose-900/60 dark:hover:text-rose-200",
    ring: "focus-visible:ring-rose-500",
    accent: "bg-rose-500",
  },
};

function AlertIcon({ variant, className }: { variant: Variant; className?: string }) {
  const common = {
    className,
    viewBox: "0 0 24 24",
    fill: "none",
    stroke: "currentColor",
    strokeWidth: 2,
    strokeLinecap: "round" as const,
    strokeLinejoin: "round" as const,
    "aria-hidden": true,
  };
  switch (variant) {
    case "success":
      return (
        <svg {...common}>
          <path d="M20 6 9 17l-5-5" />
        </svg>
      );
    case "warning":
      return (
        <svg {...common}>
          <path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0Z" />
          <path d="M12 9v4" />
          <path d="M12 17h.01" />
        </svg>
      );
    case "error":
      return (
        <svg {...common}>
          <circle cx="12" cy="12" r="10" />
          <path d="m15 9-6 6" />
          <path d="m9 9 6 6" />
        </svg>
      );
    default:
      return (
        <svg {...common}>
          <circle cx="12" cy="12" r="10" />
          <path d="M12 16v-4" />
          <path d="M12 8h.01" />
        </svg>
      );
  }
}

export default function AlertDismissible() {
  const [alerts, setAlerts] = useState<AlertItem[]>(INITIAL_ALERTS);

  function dismiss(id: string) {
    setAlerts((prev) => prev.filter((a) => a.id !== id));
  }

  function reset() {
    setAlerts(INITIAL_ALERTS);
  }

  return (
    <section className="relative w-full bg-slate-50 px-4 py-16 sm:px-6 sm:py-20 dark:bg-slate-950">
      <style>{`
        @keyframes wivalert-in {
          from { opacity: 0; transform: translateY(-8px) scale(0.985); }
          to { opacity: 1; transform: translateY(0) scale(1); }
        }
        .wivalert-item { animation: wivalert-in 320ms cubic-bezier(0.16, 1, 0.3, 1) both; }
        @media (prefers-reduced-motion: reduce) {
          .wivalert-item { animation: none; }
        }
      `}</style>

      <div className="mx-auto w-full max-w-2xl">
        <header className="mb-8 flex flex-col gap-2 sm:flex-row sm:items-end sm:justify-between">
          <div>
            <h2 className="text-2xl font-semibold tracking-tight text-slate-900 dark:text-slate-50">
              Notifications
            </h2>
            <p className="mt-1 text-sm text-slate-500 dark:text-slate-400">
              {alerts.length > 0
                ? `${alerts.length} message${alerts.length === 1 ? "" : "s"} need your attention`
                : "You're all caught up"}
            </p>
          </div>
          {alerts.length < INITIAL_ALERTS.length && (
            <button
              type="button"
              onClick={reset}
              className="inline-flex w-fit items-center gap-1.5 rounded-lg border border-slate-200 bg-white px-3 py-1.5 text-sm font-medium text-slate-600 transition-colors hover:bg-slate-100 focus:outline-none focus-visible:ring-2 focus-visible:ring-slate-400 focus-visible:ring-offset-2 focus-visible:ring-offset-slate-50 dark:border-slate-800 dark:bg-slate-900 dark:text-slate-300 dark:hover:bg-slate-800 dark:focus-visible:ring-offset-slate-950"
            >
              <svg
                className="h-3.5 w-3.5"
                viewBox="0 0 24 24"
                fill="none"
                stroke="currentColor"
                strokeWidth={2}
                strokeLinecap="round"
                strokeLinejoin="round"
                aria-hidden
              >
                <path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8" />
                <path d="M3 3v5h5" />
              </svg>
              Restore all
            </button>
          )}
        </header>

        <div className="flex flex-col gap-3">
          {alerts.map((alert) => {
            const s = VARIANT_STYLES[alert.variant];
            return (
              <div
                key={alert.id}
                role="alert"
                className={`wivalert-item relative flex gap-3 overflow-hidden rounded-xl border p-4 pr-3 shadow-sm ${s.container}`}
              >
                <span
                  className={`absolute inset-y-0 left-0 w-1 ${s.accent}`}
                  aria-hidden
                />
                <span className={`mt-0.5 shrink-0 ${s.icon}`}>
                  <AlertIcon variant={alert.variant} className="h-5 w-5" />
                </span>

                <div className="min-w-0 flex-1">
                  <p className={`text-sm font-semibold ${s.title}`}>
                    {alert.title}
                  </p>
                  <p className={`mt-0.5 text-sm leading-relaxed ${s.body}`}>
                    {alert.message}
                  </p>
                  {alert.action && (
                    <a
                      href={alert.action.href}
                      target="_blank"
                      rel="noopener"
                      className={`mt-2 inline-flex items-center gap-1 rounded text-sm font-semibold transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-offset-transparent ${s.action} ${s.ring}`}
                    >
                      {alert.action.label}
                      <svg
                        className="h-3.5 w-3.5"
                        viewBox="0 0 24 24"
                        fill="none"
                        stroke="currentColor"
                        strokeWidth={2}
                        strokeLinecap="round"
                        strokeLinejoin="round"
                        aria-hidden
                      >
                        <path d="M7 7h10v10" />
                        <path d="M7 17 17 7" />
                      </svg>
                    </a>
                  )}
                </div>

                <button
                  type="button"
                  onClick={() => dismiss(alert.id)}
                  aria-label={`Dismiss: ${alert.title}`}
                  className={`h-8 w-8 shrink-0 rounded-lg transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-offset-transparent ${s.close} ${s.ring}`}
                >
                  <svg
                    className="mx-auto h-4 w-4"
                    viewBox="0 0 24 24"
                    fill="none"
                    stroke="currentColor"
                    strokeWidth={2}
                    strokeLinecap="round"
                    strokeLinejoin="round"
                    aria-hidden
                  >
                    <path d="M18 6 6 18" />
                    <path d="m6 6 12 12" />
                  </svg>
                </button>
              </div>
            );
          })}

          {alerts.length === 0 && (
            <div className="flex flex-col items-center gap-3 rounded-xl border border-dashed border-slate-300 bg-white/60 px-6 py-12 text-center dark:border-slate-800 dark:bg-slate-900/40">
              <span className="flex h-12 w-12 items-center justify-center rounded-full bg-emerald-100 text-emerald-600 dark:bg-emerald-950/60 dark:text-emerald-400">
                <svg
                  className="h-6 w-6"
                  viewBox="0 0 24 24"
                  fill="none"
                  stroke="currentColor"
                  strokeWidth={2}
                  strokeLinecap="round"
                  strokeLinejoin="round"
                  aria-hidden
                >
                  <path d="M20 6 9 17l-5-5" />
                </svg>
              </span>
              <div>
                <p className="text-sm font-semibold text-slate-900 dark:text-slate-100">
                  Inbox zero
                </p>
                <p className="mt-0.5 text-sm text-slate-500 dark:text-slate-400">
                  Every notification has been cleared.
                </p>
              </div>
              <button
                type="button"
                onClick={reset}
                className="mt-1 rounded-lg bg-slate-900 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-slate-700 focus:outline-none focus-visible:ring-2 focus-visible:ring-slate-500 focus-visible:ring-offset-2 focus-visible:ring-offset-slate-50 dark:bg-white dark:text-slate-900 dark:hover:bg-slate-200 dark:focus-visible:ring-offset-slate-950"
              >
                Restore notifications
              </button>
            </div>
          )}
        </div>
      </div>
    </section>
  );
}

Dependencies

None (React + Tailwind only).

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 quote

Similar components

Browse all →