Web InnoventixFreeCode

Solid Variants Alert

Original · free

Solid-colour alerts across the four states.

byWeb InnoventixReact + Tailwind
alertsolidvariantsalerts
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-solid-variants.json
alert-solid-variants.tsx
"use client";

import { useState } from "react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";

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

type AlertData = {
  id: Variant;
  title: string;
  message: string;
  action: string;
};

const ALERTS: AlertData[] = [
  {
    id: "success",
    title: "Payment received",
    message: "Your invoice #INV-2048 has been paid in full. A receipt is on its way to your inbox.",
    action: "View receipt",
  },
  {
    id: "error",
    title: "Deployment failed",
    message: "Build step exited with code 1. Three tests failed in checkout.spec.ts — the release was rolled back.",
    action: "Open logs",
  },
  {
    id: "warning",
    title: "Storage almost full",
    message: "You've used 92% of your 50 GB plan. Uploads will pause once you hit the limit.",
    action: "Upgrade plan",
  },
  {
    id: "info",
    title: "Scheduled maintenance",
    message: "We'll be updating our servers on Sunday at 02:00 UTC. Expect up to 15 minutes of downtime.",
    action: "See status page",
  },
];

const STYLES: Record<
  Variant,
  { surface: string; action: string; close: string; ring: string }
> = {
  success: {
    surface: "bg-emerald-600 text-emerald-50 dark:bg-emerald-500 dark:text-emerald-950",
    action:
      "bg-emerald-50/15 text-white hover:bg-emerald-50/25 dark:bg-emerald-950/25 dark:text-emerald-950 dark:hover:bg-emerald-950/40",
    close: "hover:bg-emerald-50/20 dark:hover:bg-emerald-950/25",
    ring: "focus-visible:ring-emerald-200 dark:focus-visible:ring-emerald-900",
  },
  error: {
    surface: "bg-rose-600 text-rose-50 dark:bg-rose-500 dark:text-rose-950",
    action:
      "bg-rose-50/15 text-white hover:bg-rose-50/25 dark:bg-rose-950/25 dark:text-rose-950 dark:hover:bg-rose-950/40",
    close: "hover:bg-rose-50/20 dark:hover:bg-rose-950/25",
    ring: "focus-visible:ring-rose-200 dark:focus-visible:ring-rose-900",
  },
  warning: {
    surface: "bg-amber-500 text-amber-950 dark:bg-amber-400 dark:text-amber-950",
    action:
      "bg-amber-950/10 text-amber-950 hover:bg-amber-950/20 dark:bg-amber-950/15 dark:hover:bg-amber-950/25",
    close: "hover:bg-amber-950/15",
    ring: "focus-visible:ring-amber-800 dark:focus-visible:ring-amber-900",
  },
  info: {
    surface: "bg-sky-600 text-sky-50 dark:bg-sky-500 dark:text-sky-950",
    action:
      "bg-sky-50/15 text-white hover:bg-sky-50/25 dark:bg-sky-950/25 dark:text-sky-950 dark:hover:bg-sky-950/40",
    close: "hover:bg-sky-50/20 dark:hover:bg-sky-950/25",
    ring: "focus-visible:ring-sky-200 dark:focus-visible:ring-sky-900",
  },
};

function Icon({ variant }: { variant: Variant }) {
  const common = {
    width: 22,
    height: 22,
    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 "error":
      return (
        <svg {...common}>
          <circle cx="12" cy="12" r="9" />
          <path d="m15 9-6 6M9 9l6 6" />
        </svg>
      );
    case "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 9v4M12 17h.01" />
        </svg>
      );
    case "info":
      return (
        <svg {...common}>
          <circle cx="12" cy="12" r="9" />
          <path d="M12 16v-4M12 8h.01" />
        </svg>
      );
  }
}

const LABELS: Record<Variant, string> = {
  success: "Success",
  error: "Error",
  warning: "Warning",
  info: "Information",
};

export default function AlertSolidVariants() {
  const reduce = useReducedMotion();
  const [open, setOpen] = useState<Record<Variant, boolean>>({
    success: true,
    error: true,
    warning: true,
    info: true,
  });
  const [note, setNote] = useState<string>("");

  const dismiss = (id: Variant) =>
    setOpen((prev) => ({ ...prev, [id]: false }));

  const allClosed = ALERTS.every((a) => !open[a.id]);

  return (
    <section className="relative w-full bg-slate-50 px-4 py-20 sm:px-6 dark:bg-slate-950">
      <style>{`
        @keyframes asv-pop {
          0% { transform: scale(.96); opacity: 0; }
          100% { transform: scale(1); opacity: 1; }
        }
        @media (prefers-reduced-motion: reduce) {
          .asv-pop { animation: none !important; }
        }
      `}</style>

      <div className="mx-auto max-w-2xl">
        <header className="mb-10 text-center">
          <p className="text-xs font-semibold uppercase tracking-[0.2em] text-indigo-600 dark:text-indigo-400">
            Feedback
          </p>
          <h2 className="mt-2 text-3xl font-bold tracking-tight text-slate-900 dark:text-slate-50">
            Solid alerts
          </h2>
          <p className="mx-auto mt-3 max-w-md text-sm text-slate-600 dark:text-slate-400">
            Four filled states for confirmations, failures, cautions and notices. Each one dismisses and carries a primary action.
          </p>
        </header>

        <div
          className="flex flex-col gap-4"
          role="region"
          aria-label="Alert examples"
        >
          <AnimatePresence mode="popLayout">
            {ALERTS.filter((a) => open[a.id]).map((alert) => {
              const s = STYLES[alert.id];
              return (
                <motion.div
                  key={alert.id}
                  layout={!reduce}
                  role="alert"
                  initial={reduce ? false : { opacity: 0, y: -8, scale: 0.98 }}
                  animate={{ opacity: 1, y: 0, scale: 1 }}
                  exit={
                    reduce
                      ? { opacity: 0 }
                      : { opacity: 0, x: 24, scale: 0.96, transition: { duration: 0.2 } }
                  }
                  transition={{ type: "spring", stiffness: 460, damping: 34 }}
                  className={`asv-pop flex items-start gap-3 rounded-2xl p-4 shadow-lg shadow-slate-900/5 ${s.surface}`}
                >
                  <span className="mt-0.5 shrink-0" aria-hidden>
                    <Icon variant={alert.id} />
                  </span>

                  <div className="min-w-0 flex-1">
                    <p className="text-sm font-semibold leading-tight">
                      <span className="sr-only">{LABELS[alert.id]}: </span>
                      {alert.title}
                    </p>
                    <p className="mt-1 text-sm leading-relaxed opacity-90">
                      {alert.message}
                    </p>
                    <div className="mt-3">
                      <button
                        type="button"
                        onClick={() =>
                          setNote(`${alert.action} — ${LABELS[alert.id]}`)
                        }
                        className={`inline-flex items-center rounded-lg px-3 py-1.5 text-xs font-semibold outline-none transition-colors focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-offset-transparent ${s.action} ${s.ring}`}
                      >
                        {alert.action}
                      </button>
                    </div>
                  </div>

                  <button
                    type="button"
                    onClick={() => dismiss(alert.id)}
                    aria-label={`Dismiss ${LABELS[alert.id]} alert`}
                    className={`-mr-1 -mt-1 shrink-0 rounded-lg p-1.5 outline-none transition-colors focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-offset-transparent ${s.close} ${s.ring}`}
                  >
                    <svg
                      width={18}
                      height={18}
                      viewBox="0 0 24 24"
                      fill="none"
                      stroke="currentColor"
                      strokeWidth={2}
                      strokeLinecap="round"
                      strokeLinejoin="round"
                      aria-hidden
                    >
                      <path d="M18 6 6 18M6 6l12 12" />
                    </svg>
                  </button>
                </motion.div>
              );
            })}
          </AnimatePresence>

          {allClosed && (
            <div className="rounded-2xl border border-dashed border-slate-300 p-6 text-center text-sm text-slate-500 dark:border-slate-700 dark:text-slate-400">
              All alerts dismissed.
              <button
                type="button"
                onClick={() =>
                  setOpen({ success: true, error: true, warning: true, info: true })
                }
                className="ml-1 rounded font-semibold text-indigo-600 underline underline-offset-2 outline-none focus-visible:ring-2 focus-visible:ring-indigo-400 dark:text-indigo-400"
              >
                Reset
              </button>
            </div>
          )}
        </div>

        <p
          aria-live="polite"
          className="mt-6 h-5 text-center text-xs font-medium text-slate-500 dark:text-slate-400"
        >
          {note ? `Action triggered: ${note}` : ""}
        </p>
      </div>
    </section>
  );
}

Dependencies

motion

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 →