Web InnoventixFreeCode

Soft Variants Alert

Original · free

Soft-tinted alerts: success, info, warning and error, each with an icon.

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

import { useState } from "react";

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

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

const ALERTS: AlertItem[] = [
  {
    id: "success",
    variant: "success",
    title: "Payment received",
    message: "Your invoice #INV-2094 for $1,280.00 was paid. A receipt is on its way to your inbox.",
    action: "View receipt",
  },
  {
    id: "info",
    variant: "info",
    title: "Scheduled maintenance",
    message: "We'll be upgrading our servers on Sunday, 02:00-04:00 UTC. Expect brief interruptions to the dashboard.",
    action: "Read details",
  },
  {
    id: "warning",
    variant: "warning",
    title: "Storage almost full",
    message: "You've used 92% of your 50 GB plan. Uploads will be paused once you hit the limit.",
    action: "Upgrade plan",
  },
  {
    id: "error",
    variant: "error",
    title: "Deployment failed",
    message: "Build step 'compile' exited with code 1. Check the logs to see which module broke the pipeline.",
    action: "Open logs",
  },
];

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

function AlertIcon({ variant }: { variant: Variant }) {
  const common = {
    viewBox: "0 0 24 24",
    fill: "none",
    stroke: "currentColor",
    strokeWidth: 2,
    strokeLinecap: "round" as const,
    strokeLinejoin: "round" as const,
    className: "h-5 w-5",
    "aria-hidden": true,
  };

  if (variant === "success") {
    return (
      <svg {...common}>
        <path d="M20 6 9 17l-5-5" />
      </svg>
    );
  }
  if (variant === "info") {
    return (
      <svg {...common}>
        <circle cx="12" cy="12" r="9" />
        <path d="M12 11v5" />
        <path d="M12 8h.01" />
      </svg>
    );
  }
  if (variant === "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>
    );
  }
  return (
    <svg {...common}>
      <circle cx="12" cy="12" r="9" />
      <path d="m15 9-6 6" />
      <path d="m9 9 6 6" />
    </svg>
  );
}

export default function AlertSoftVariants() {
  const [dismissed, setDismissed] = useState<Record<string, boolean>>({});
  const [statusText, setStatusText] = useState<string>("");

  const visible = ALERTS.filter((a) => !dismissed[a.id]);

  const dismiss = (item: AlertItem) => {
    setDismissed((prev) => ({ ...prev, [item.id]: true }));
    setStatusText(`${item.title} alert dismissed.`);
  };

  const restore = () => {
    setDismissed({});
    setStatusText("All alerts restored.");
  };

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

      <div className="mx-auto max-w-2xl">
        <div className="mb-8 flex flex-wrap items-end justify-between gap-4">
          <div>
            <h2 className="text-2xl font-semibold tracking-tight text-zinc-900 dark:text-zinc-50">
              Notifications
            </h2>
            <p className="mt-1 text-sm text-zinc-500 dark:text-zinc-400">
              Soft-tinted alerts for every state.
            </p>
          </div>
          {visible.length < ALERTS.length ? (
            <button
              type="button"
              onClick={restore}
              className="rounded-lg border border-zinc-200 bg-white px-3 py-1.5 text-sm font-medium text-zinc-700 transition-colors hover:bg-zinc-50 focus:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500/60 dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-200 dark:hover:bg-zinc-800"
            >
              Restore all
            </button>
          ) : null}
        </div>

        <div className="space-y-3">
          {visible.map((item) => {
            const s = STYLES[item.variant];
            return (
              <div
                key={item.id}
                role="alert"
                className={`asv-item flex items-start gap-3 rounded-xl border p-4 ${s.container}`}
              >
                <span
                  className={`mt-0.5 flex h-9 w-9 flex-none items-center justify-center rounded-lg ${s.iconWrap}`}
                >
                  <AlertIcon variant={item.variant} />
                </span>

                <div className="min-w-0 flex-1">
                  <p className={`text-sm font-semibold ${s.title}`}>
                    {item.title}
                  </p>
                  <p className={`mt-1 text-sm leading-relaxed ${s.body}`}>
                    {item.message}
                  </p>
                  <button
                    type="button"
                    onClick={() =>
                      setStatusText(`${item.action} selected for "${item.title}".`)
                    }
                    className={`mt-2 rounded text-sm font-semibold underline-offset-2 transition-colors hover:underline focus:outline-none focus-visible:ring-2 focus-visible:rounded ${s.action} ${s.ring}`}
                  >
                    {item.action}
                  </button>
                </div>

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

          {visible.length === 0 ? (
            <div className="rounded-xl border border-dashed border-zinc-300 bg-zinc-50 p-8 text-center dark:border-zinc-700 dark:bg-zinc-900/50">
              <p className="text-sm font-medium text-zinc-600 dark:text-zinc-300">
                You're all caught up.
              </p>
              <button
                type="button"
                onClick={restore}
                className="mt-3 rounded-lg bg-zinc-900 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-zinc-700 focus:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500/60 dark:bg-white dark:text-zinc-900 dark:hover:bg-zinc-200"
              >
                Restore alerts
              </button>
            </div>
          ) : null}
        </div>

        <p className="sr-only" role="status" aria-live="polite">
          {statusText}
        </p>
      </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 →