Web InnoventixFreeCode

Icon Large Alert

Original · free

An alert led by a large circular icon.

byWeb InnoventixReact + Tailwind
alerticonlargealerts
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-icon-large.json
alert-icon-large.tsx
"use client";

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

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

type AlertItem = {
  id: string;
  tone: Tone;
  title: string;
  body: string;
  action: string;
  dismiss: string;
};

const TONES: Record<
  Tone,
  {
    ring: string;
    iconWrap: string;
    iconText: string;
    accent: string;
    action: string;
    focus: string;
    pulse: string;
    icon: ReactNode;
  }
> = {
  info: {
    ring: "border-sky-200/80 dark:border-sky-500/25",
    iconWrap: "bg-sky-100 dark:bg-sky-500/15 ring-sky-200/70 dark:ring-sky-400/20",
    iconText: "text-sky-600 dark:text-sky-300",
    accent: "text-sky-900 dark:text-sky-100",
    action:
      "bg-sky-600 text-white hover:bg-sky-500 dark:bg-sky-500 dark:hover:bg-sky-400",
    focus: "focus-visible:outline-sky-500",
    pulse: "bg-sky-400/40 dark:bg-sky-400/25",
    icon: (
      <path
        strokeLinecap="round"
        strokeLinejoin="round"
        d="M12 9.5v4.75M12 7.25h.01M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"
      />
    ),
  },
  success: {
    ring: "border-emerald-200/80 dark:border-emerald-500/25",
    iconWrap:
      "bg-emerald-100 dark:bg-emerald-500/15 ring-emerald-200/70 dark:ring-emerald-400/20",
    iconText: "text-emerald-600 dark:text-emerald-300",
    accent: "text-emerald-900 dark:text-emerald-100",
    action:
      "bg-emerald-600 text-white hover:bg-emerald-500 dark:bg-emerald-500 dark:hover:bg-emerald-400",
    focus: "focus-visible:outline-emerald-500",
    pulse: "bg-emerald-400/40 dark:bg-emerald-400/25",
    icon: (
      <path
        strokeLinecap="round"
        strokeLinejoin="round"
        d="m8 12.5 2.75 2.75L16.5 9.5M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"
      />
    ),
  },
  warning: {
    ring: "border-amber-200/80 dark:border-amber-500/25",
    iconWrap:
      "bg-amber-100 dark:bg-amber-500/15 ring-amber-200/70 dark:ring-amber-400/20",
    iconText: "text-amber-600 dark:text-amber-300",
    accent: "text-amber-900 dark:text-amber-100",
    action:
      "bg-amber-500 text-white hover:bg-amber-400 dark:bg-amber-500 dark:hover:bg-amber-400",
    focus: "focus-visible:outline-amber-500",
    pulse: "bg-amber-400/40 dark:bg-amber-400/25",
    icon: (
      <path
        strokeLinecap="round"
        strokeLinejoin="round"
        d="M12 9.25v3.5M12 16h.01M10.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"
      />
    ),
  },
  error: {
    ring: "border-rose-200/80 dark:border-rose-500/25",
    iconWrap: "bg-rose-100 dark:bg-rose-500/15 ring-rose-200/70 dark:ring-rose-400/20",
    iconText: "text-rose-600 dark:text-rose-300",
    accent: "text-rose-900 dark:text-rose-100",
    action:
      "bg-rose-600 text-white hover:bg-rose-500 dark:bg-rose-500 dark:hover:bg-rose-400",
    focus: "focus-visible:outline-rose-500",
    pulse: "bg-rose-400/40 dark:bg-rose-400/25",
    icon: (
      <path
        strokeLinecap="round"
        strokeLinejoin="round"
        d="M15 9l-6 6m0-6 6 6M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"
      />
    ),
  },
};

const INITIAL: AlertItem[] = [
  {
    id: "backup",
    tone: "success",
    title: "Nightly backup completed",
    body: "All 14 project databases were snapshotted and verified at 03:12 UTC. The last restore point is now under 6 hours old.",
    action: "View restore points",
    dismiss: "Dismiss",
  },
  {
    id: "seats",
    tone: "warning",
    title: "You're almost out of seats",
    body: "9 of 10 seats on the Growth plan are in use. Invite one more teammate and the next invite will require an upgrade.",
    action: "Manage plan",
    dismiss: "Remind me later",
  },
  {
    id: "webhook",
    tone: "error",
    title: "Webhook delivery is failing",
    body: "The endpoint https://api.acme.co/hooks returned 502 on the last 5 attempts. Deliveries are paused until it responds with a 2xx.",
    action: "Inspect logs",
    dismiss: "Ignore",
  },
  {
    id: "digest",
    tone: "info",
    title: "Your weekly digest is ready",
    body: "Traffic to your docs grew 18% and three articles crossed 1,000 reads this week. The full breakdown is waiting in your inbox.",
    action: "Open digest",
    dismiss: "Close",
  },
];

export default function AlertIconLarge() {
  const prefersReduced = useReducedMotion();
  const [items, setItems] = useState<AlertItem[]>(INITIAL);

  const dismiss = (id: string) =>
    setItems((prev) => prev.filter((item) => item.id !== id));

  const reset = () => setItems(INITIAL);

  return (
    <section className="relative w-full bg-white px-4 py-20 dark:bg-neutral-950 sm:px-6 lg:px-8">
      <style>{`
        @keyframes ail-ripple {
          0%   { transform: scale(0.85); opacity: 0.55; }
          70%  { transform: scale(1.9); opacity: 0; }
          100% { transform: scale(1.9); opacity: 0; }
        }
        @media (prefers-reduced-motion: reduce) {
          .ail-ripple { animation: none !important; }
        }
      `}</style>

      <div className="mx-auto max-w-2xl">
        <header className="mb-10 flex flex-col gap-2">
          <span className="text-xs font-semibold uppercase tracking-[0.18em] text-indigo-600 dark:text-indigo-400">
            Notifications
          </span>
          <h2 className="text-2xl font-semibold tracking-tight text-neutral-900 dark:text-neutral-50 sm:text-3xl">
            Everything that needs your eyes
          </h2>
          <p className="text-sm text-neutral-500 dark:text-neutral-400">
            Four live alerts, each led by a large status icon. Dismiss what you've
            handled — the stack settles smoothly.
          </p>
        </header>

        <div className="flex flex-col gap-4" role="region" aria-label="Alerts">
          <AnimatePresence initial={false} mode="popLayout">
            {items.map((item) => {
              const t = TONES[item.tone];
              return (
                <motion.div
                  key={item.id}
                  layout={!prefersReduced}
                  initial={
                    prefersReduced
                      ? { opacity: 0 }
                      : { opacity: 0, y: 12, scale: 0.98 }
                  }
                  animate={
                    prefersReduced
                      ? { opacity: 1 }
                      : { opacity: 1, y: 0, scale: 1 }
                  }
                  exit={
                    prefersReduced
                      ? { opacity: 0 }
                      : { opacity: 0, x: 24, scale: 0.97 }
                  }
                  transition={{ duration: 0.28, ease: [0.22, 1, 0.36, 1] }}
                  role="alert"
                  className={`relative flex gap-4 overflow-hidden rounded-2xl border ${t.ring} bg-white/90 p-4 shadow-sm ring-1 ring-black/[0.02] backdrop-blur-sm dark:bg-neutral-900/70 dark:ring-white/5 sm:gap-5 sm:p-5`}
                >
                  <div className="relative flex-shrink-0">
                    {!prefersReduced && item.tone !== "success" && (
                      <span
                        aria-hidden="true"
                        className={`ail-ripple pointer-events-none absolute inset-0 rounded-full ${t.pulse}`}
                        style={{
                          animation: "ail-ripple 2.6s ease-out infinite",
                        }}
                      />
                    )}
                    <div
                      className={`relative flex h-14 w-14 items-center justify-center rounded-full ring-1 ${t.iconWrap} sm:h-16 sm:w-16`}
                    >
                      <svg
                        viewBox="0 0 24 24"
                        fill="none"
                        stroke="currentColor"
                        strokeWidth={1.6}
                        aria-hidden="true"
                        className={`h-7 w-7 ${t.iconText} sm:h-8 sm:w-8`}
                      >
                        {t.icon}
                      </svg>
                    </div>
                  </div>

                  <div className="flex min-w-0 flex-1 flex-col">
                    <h3
                      className={`text-sm font-semibold ${t.accent} sm:text-base`}
                    >
                      {item.title}
                    </h3>
                    <p className="mt-1 text-sm leading-relaxed text-neutral-600 dark:text-neutral-300">
                      {item.body}
                    </p>

                    <div className="mt-4 flex flex-wrap items-center gap-2">
                      <button
                        type="button"
                        className={`inline-flex items-center rounded-lg px-3 py-1.5 text-xs font-semibold shadow-sm outline-none transition focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 ${t.action} ${t.focus}`}
                      >
                        {item.action}
                      </button>
                      <button
                        type="button"
                        onClick={() => dismiss(item.id)}
                        className="inline-flex items-center rounded-lg px-3 py-1.5 text-xs font-semibold text-neutral-600 outline-none transition hover:bg-neutral-100 hover:text-neutral-900 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-neutral-400 dark:text-neutral-300 dark:hover:bg-neutral-800 dark:hover:text-neutral-50"
                      >
                        {item.dismiss}
                      </button>
                    </div>
                  </div>

                  <button
                    type="button"
                    onClick={() => dismiss(item.id)}
                    aria-label={`Dismiss alert: ${item.title}`}
                    className="absolute right-3 top-3 flex h-7 w-7 items-center justify-center rounded-md text-neutral-400 outline-none transition hover:bg-neutral-100 hover:text-neutral-700 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-neutral-400 dark:text-neutral-500 dark:hover:bg-neutral-800 dark:hover:text-neutral-200"
                  >
                    <svg
                      viewBox="0 0 24 24"
                      fill="none"
                      stroke="currentColor"
                      strokeWidth={1.8}
                      aria-hidden="true"
                      className="h-4 w-4"
                    >
                      <path
                        strokeLinecap="round"
                        strokeLinejoin="round"
                        d="M6 6l12 12M18 6 6 18"
                      />
                    </svg>
                  </button>
                </motion.div>
              );
            })}
          </AnimatePresence>
        </div>

        <AnimatePresence>
          {items.length === 0 && (
            <motion.div
              initial={prefersReduced ? { opacity: 0 } : { opacity: 0, y: 8 }}
              animate={{ opacity: 1, y: 0 }}
              exit={{ opacity: 0 }}
              transition={{ duration: 0.25 }}
              className="flex flex-col items-center gap-4 rounded-2xl border border-dashed border-neutral-200 py-14 text-center dark:border-neutral-800"
            >
              <div className="flex h-14 w-14 items-center justify-center rounded-full bg-emerald-100 text-emerald-600 dark:bg-emerald-500/15 dark:text-emerald-300">
                <svg
                  viewBox="0 0 24 24"
                  fill="none"
                  stroke="currentColor"
                  strokeWidth={1.6}
                  aria-hidden="true"
                  className="h-7 w-7"
                >
                  <path
                    strokeLinecap="round"
                    strokeLinejoin="round"
                    d="m8 12.5 2.75 2.75L16.5 9.5M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"
                  />
                </svg>
              </div>
              <div>
                <p className="text-sm font-semibold text-neutral-900 dark:text-neutral-50">
                  Inbox zero
                </p>
                <p className="mt-1 text-sm text-neutral-500 dark:text-neutral-400">
                  You've cleared every alert. Nice work.
                </p>
              </div>
              <button
                type="button"
                onClick={reset}
                className="inline-flex items-center rounded-lg bg-neutral-900 px-3.5 py-2 text-xs font-semibold text-white outline-none transition hover:bg-neutral-700 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-neutral-500 dark:bg-neutral-50 dark:text-neutral-900 dark:hover:bg-neutral-200"
              >
                Restore alerts
              </button>
            </motion.div>
          )}
        </AnimatePresence>
      </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 →