Web InnoventixFreeCode

Outline Variants Alert

Original · free

Outlined alerts across the four states.

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

import { useState } from "react";

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

interface AlertConfig {
  id: string;
  state: AlertState;
  title: string;
  body: string;
  action: string;
}

const ALERTS: AlertConfig[] = [
  {
    id: "success",
    state: "success",
    title: "Payment received",
    body: "Your invoice #INV-2047 was settled. A receipt is on its way to your inbox.",
    action: "View receipt",
  },
  {
    id: "error",
    state: "error",
    title: "Deployment failed",
    body: "Build step exited with code 1. Check the logs for the failing test in api/auth.",
    action: "Open logs",
  },
  {
    id: "warning",
    state: "warning",
    title: "Storage almost full",
    body: "You have used 92% of your plan's 50 GB. Free up space before uploads pause.",
    action: "Manage storage",
  },
  {
    id: "info",
    state: "info",
    title: "Scheduled maintenance",
    body: "We are updating our servers on Sunday at 02:00 UTC. Expect a brief downtime.",
    action: "Read more",
  },
];

const STYLES: Record<
  AlertState,
  {
    container: string;
    icon: string;
    title: string;
    body: string;
    action: string;
    close: string;
    ring: string;
  }
> = {
  success: {
    container:
      "border-emerald-300 bg-emerald-50/60 dark:border-emerald-500/40 dark:bg-emerald-500/5",
    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 dark:text-emerald-400 dark:hover:bg-emerald-500/15",
    ring: "focus-visible:ring-emerald-500/60",
  },
  error: {
    container:
      "border-rose-300 bg-rose-50/60 dark:border-rose-500/40 dark:bg-rose-500/5",
    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 dark:text-rose-400 dark:hover:bg-rose-500/15",
    ring: "focus-visible:ring-rose-500/60",
  },
  warning: {
    container:
      "border-amber-300 bg-amber-50/60 dark:border-amber-500/40 dark:bg-amber-500/5",
    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 dark:text-amber-400 dark:hover:bg-amber-500/15",
    ring: "focus-visible:ring-amber-500/60",
  },
  info: {
    container:
      "border-sky-300 bg-sky-50/60 dark:border-sky-500/40 dark:bg-sky-500/5",
    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 dark:text-sky-400 dark:hover:bg-sky-500/15",
    ring: "focus-visible:ring-sky-500/60",
  },
};

function AlertIcon({ state, className }: { state: AlertState; className?: string }) {
  const common = {
    width: 20,
    height: 20,
    viewBox: "0 0 24 24",
    fill: "none",
    stroke: "currentColor",
    strokeWidth: 2,
    strokeLinecap: "round" as const,
    strokeLinejoin: "round" as const,
    className,
    "aria-hidden": true,
  };
  if (state === "success") {
    return (
      <svg {...common}>
        <circle cx="12" cy="12" r="9" />
        <path d="m8.5 12 2.5 2.5 4.5-5" />
      </svg>
    );
  }
  if (state === "error") {
    return (
      <svg {...common}>
        <circle cx="12" cy="12" r="9" />
        <path d="M15 9l-6 6M9 9l6 6" />
      </svg>
    );
  }
  if (state === "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>
    );
  }
  return (
    <svg {...common}>
      <circle cx="12" cy="12" r="9" />
      <path d="M12 11v5" />
      <path d="M12 8h.01" />
    </svg>
  );
}

export default function AlertOutlineVariants() {
  const [visible, setVisible] = useState<Record<string, boolean>>({
    success: true,
    error: true,
    warning: true,
    info: true,
  });

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

  const resetAll = () => {
    setVisible({ success: true, error: true, warning: true, info: true });
  };

  const allHidden = ALERTS.every((a) => !visible[a.id]);

  return (
    <section className="relative w-full bg-white px-4 py-16 sm:px-6 sm:py-20 dark:bg-neutral-950">
      <style>{`
        @keyframes aov-enter {
          0% { opacity: 0; transform: translateY(-8px) scale(0.985); }
          100% { opacity: 1; transform: translateY(0) scale(1); }
        }
        .aov-item { animation: aov-enter 0.35s cubic-bezier(0.22, 1, 0.36, 1) both; }
        @media (prefers-reduced-motion: reduce) {
          .aov-item { animation: none; }
        }
      `}</style>

      <div className="mx-auto max-w-2xl">
        <div className="mb-8 flex items-end justify-between gap-4">
          <div>
            <p className="text-xs font-semibold uppercase tracking-widest text-indigo-600 dark:text-indigo-400">
              Feedback
            </p>
            <h2 className="mt-1 text-2xl font-bold tracking-tight text-neutral-900 dark:text-neutral-50">
              Outlined alerts
            </h2>
            <p className="mt-1 text-sm text-neutral-500 dark:text-neutral-400">
              Four states, each dismissible with a primary action.
            </p>
          </div>
          {allHidden && (
            <button
              type="button"
              onClick={resetAll}
              className="shrink-0 rounded-lg border border-neutral-300 bg-white px-3 py-1.5 text-xs font-medium text-neutral-700 transition-colors hover:bg-neutral-100 focus:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500/60 dark:border-neutral-700 dark:bg-neutral-900 dark:text-neutral-200 dark:hover:bg-neutral-800"
            >
              Restore all
            </button>
          )}
        </div>

        <div className="space-y-3">
          {ALERTS.map((alert) => {
            if (!visible[alert.id]) return null;
            const s = STYLES[alert.state];
            return (
              <div
                key={alert.id}
                role="alert"
                className={`aov-item flex items-start gap-3 rounded-xl border px-4 py-3.5 ${s.container}`}
              >
                <span className={`mt-0.5 shrink-0 ${s.icon}`}>
                  <AlertIcon state={alert.state} />
                </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.body}
                  </p>
                  <button
                    type="button"
                    onClick={() => dismiss(alert.id)}
                    className={`mt-2 rounded text-sm font-semibold underline-offset-4 transition-colors hover:underline focus:outline-none focus-visible:ring-2 ${s.action} ${s.ring}`}
                  >
                    {alert.action}
                  </button>
                </div>

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

        {allHidden && (
          <div className="rounded-xl border border-dashed border-neutral-300 px-4 py-10 text-center text-sm text-neutral-500 dark:border-neutral-700 dark:text-neutral-400">
            All alerts dismissed.
          </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 →