Web InnoventixFreeCode

Glass Alert

Original · free

A frosted-glass alert over a subtle gradient.

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

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

type GlassAlert = {
  id: string;
  tone: "info" | "success" | "warning" | "danger";
  title: string;
  body: string;
  action: string;
};

const SEED: GlassAlert[] = [
  {
    id: "deploy",
    tone: "success",
    title: "Deploy shipped to production",
    body: "Build 4.12.0 rolled out to all 3 regions in 47s. Error rate is flat at 0.02%.",
    action: "View release",
  },
  {
    id: "billing",
    tone: "warning",
    title: "Card expires this month",
    body: "The Visa ending 4471 on your workspace expires 07/26. Update it to avoid a failed renewal.",
    action: "Update payment",
  },
  {
    id: "invite",
    tone: "info",
    title: "3 teammates are waiting",
    body: "Priya, Marcus and Wei requested access to the Growth workspace 2 days ago.",
    action: "Review requests",
  },
  {
    id: "quota",
    tone: "danger",
    title: "API quota nearly exhausted",
    body: "You have used 94% of this month's 500k requests. Throttling begins at 100%.",
    action: "Raise limit",
  },
];

const TONE: Record<
  GlassAlert["tone"],
  {
    ring: string;
    glow: string;
    dot: string;
    chip: string;
    label: string;
    icon: ReactElement;
  }
> = {
  info: {
    ring: "ring-sky-400/40 dark:ring-sky-300/25",
    glow: "from-sky-400/25 via-sky-300/10",
    dot: "bg-sky-500 dark:bg-sky-400",
    chip:
      "bg-sky-500/15 text-sky-700 ring-sky-500/25 dark:bg-sky-400/15 dark:text-sky-200 dark:ring-sky-300/25",
    label: "Info",
    icon: (
      <path
        strokeLinecap="round"
        strokeLinejoin="round"
        d="M12 9h.01M11 12h1v4h1m-1-10a9 9 0 100 18 9 9 0 000-18z"
      />
    ),
  },
  success: {
    ring: "ring-emerald-400/40 dark:ring-emerald-300/25",
    glow: "from-emerald-400/25 via-emerald-300/10",
    dot: "bg-emerald-500 dark:bg-emerald-400",
    chip:
      "bg-emerald-500/15 text-emerald-700 ring-emerald-500/25 dark:bg-emerald-400/15 dark:text-emerald-200 dark:ring-emerald-300/25",
    label: "Success",
    icon: (
      <path
        strokeLinecap="round"
        strokeLinejoin="round"
        d="M9 12.5l2 2 4-4.5M12 3a9 9 0 100 18 9 9 0 000-18z"
      />
    ),
  },
  warning: {
    ring: "ring-amber-400/40 dark:ring-amber-300/25",
    glow: "from-amber-400/25 via-amber-300/10",
    dot: "bg-amber-500 dark:bg-amber-400",
    chip:
      "bg-amber-500/15 text-amber-700 ring-amber-500/25 dark:bg-amber-400/15 dark:text-amber-200 dark:ring-amber-300/25",
    label: "Warning",
    icon: (
      <path
        strokeLinecap="round"
        strokeLinejoin="round"
        d="M12 9v4m0 3h.01M10.3 4.2L2.6 17.5A2 2 0 004.3 20.5h15.4a2 2 0 001.7-3L13.7 4.2a2 2 0 00-3.4 0z"
      />
    ),
  },
  danger: {
    ring: "ring-rose-400/40 dark:ring-rose-300/25",
    glow: "from-rose-400/25 via-rose-300/10",
    dot: "bg-rose-500 dark:bg-rose-400",
    chip:
      "bg-rose-500/15 text-rose-700 ring-rose-500/25 dark:bg-rose-400/15 dark:text-rose-200 dark:ring-rose-300/25",
    label: "Critical",
    icon: (
      <path
        strokeLinecap="round"
        strokeLinejoin="round"
        d="M12 8v4m0 4h.01M12 3a9 9 0 100 18 9 9 0 000-18z"
      />
    ),
  },
};

export default function AlertGlass() {
  const prefersReduced = useReducedMotion();
  const [alerts, setAlerts] = useState<GlassAlert[]>(SEED);
  const [resolved, setResolved] = useState<string | null>(null);

  const dismiss = (id: string) =>
    setAlerts((prev) => prev.filter((a) => a.id !== id));

  const act = (a: GlassAlert) => {
    setResolved(a.action);
    dismiss(a.id);
    window.setTimeout(() => setResolved(null), 2600);
  };

  const reset = () => {
    setResolved(null);
    setAlerts(SEED);
  };

  return (
    <section className="relative w-full overflow-hidden bg-slate-50 px-5 py-20 sm:px-8 sm:py-24 dark:bg-slate-950">
      <style>{`
        @keyframes agls_drift {
          0%   { transform: translate3d(0,0,0) scale(1); }
          50%  { transform: translate3d(4%,-3%,0) scale(1.08); }
          100% { transform: translate3d(0,0,0) scale(1); }
        }
        @keyframes agls_sheen {
          0%   { transform: translateX(-120%); }
          60%  { transform: translateX(220%); }
          100% { transform: translateX(220%); }
        }
        @media (prefers-reduced-motion: reduce) {
          .agls_drift, .agls_sheen { animation: none !important; }
        }
      `}</style>

      {/* gradient backdrop */}
      <div aria-hidden className="pointer-events-none absolute inset-0">
        <div className="absolute inset-0 bg-gradient-to-br from-indigo-100 via-slate-50 to-sky-100 dark:from-indigo-950/60 dark:via-slate-950 dark:to-sky-950/50" />
        <div className="agls_drift absolute -left-24 top-0 h-72 w-72 rounded-full bg-violet-400/40 blur-3xl dark:bg-violet-600/30" style={{ animation: "agls_drift 18s ease-in-out infinite" }} />
        <div className="agls_drift absolute right-0 top-24 h-80 w-80 rounded-full bg-sky-400/40 blur-3xl dark:bg-sky-600/25" style={{ animation: "agls_drift 22s ease-in-out infinite reverse" }} />
        <div className="agls_drift absolute bottom-0 left-1/3 h-64 w-64 rounded-full bg-emerald-300/40 blur-3xl dark:bg-emerald-600/20" style={{ animation: "agls_drift 26s ease-in-out infinite" }} />
      </div>

      <div className="relative mx-auto flex max-w-2xl flex-col gap-4">
        <header className="mb-2 flex items-end justify-between gap-4">
          <div>
            <h2 className="text-lg font-semibold tracking-tight text-slate-900 dark:text-white">
              Notifications
            </h2>
            <p className="text-sm text-slate-600 dark:text-slate-400">
              {alerts.length > 0
                ? `${alerts.length} item${alerts.length > 1 ? "s" : ""} need your attention`
                : "You're all caught up."}
            </p>
          </div>
          <button
            type="button"
            onClick={reset}
            className="rounded-full px-3 py-1.5 text-xs font-medium text-slate-600 ring-1 ring-slate-300/70 transition hover:bg-white/60 hover:text-slate-900 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 dark:text-slate-300 dark:ring-white/15 dark:hover:bg-white/10 dark:hover:text-white"
          >
            Restore all
          </button>
        </header>

        <div
          className="sr-only"
          role="status"
          aria-live="polite"
        >
          {resolved ? `${resolved} action confirmed.` : ""}
        </div>

        <AnimatePresence mode="popLayout">
          {resolved && (
            <motion.div
              key="toast"
              layout={!prefersReduced}
              initial={prefersReduced ? false : { opacity: 0, y: -8 }}
              animate={{ opacity: 1, y: 0 }}
              exit={prefersReduced ? { opacity: 0 } : { opacity: 0, y: -8 }}
              className="flex items-center gap-2 rounded-2xl border border-emerald-400/30 bg-emerald-400/15 px-4 py-3 text-sm font-medium text-emerald-800 backdrop-blur-md dark:text-emerald-200"
            >
              <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.8} className="h-4 w-4 shrink-0">
                <path strokeLinecap="round" strokeLinejoin="round" d="M9 12.5l2 2 4-4.5" />
              </svg>
              Done — “{resolved}” confirmed.
            </motion.div>
          )}

          {alerts.map((a) => {
            const t = TONE[a.tone];
            return (
              <motion.article
                key={a.id}
                layout={!prefersReduced}
                initial={prefersReduced ? false : { opacity: 0, y: 12, scale: 0.98 }}
                animate={{ opacity: 1, y: 0, scale: 1 }}
                exit={
                  prefersReduced
                    ? { opacity: 0 }
                    : { opacity: 0, x: 40, scale: 0.96, transition: { duration: 0.22 } }
                }
                transition={{ type: "spring", stiffness: 380, damping: 32 }}
                role="alert"
                aria-label={`${t.label}: ${a.title}`}
                className={`group relative overflow-hidden rounded-2xl border border-white/40 bg-white/50 p-4 shadow-lg shadow-slate-900/5 ring-1 backdrop-blur-xl sm:p-5 dark:border-white/10 dark:bg-white/5 dark:shadow-black/30 ${t.ring}`}
              >
                {/* tone glow */}
                <div
                  aria-hidden
                  className={`pointer-events-none absolute -left-16 -top-16 h-40 w-40 rounded-full bg-gradient-to-br to-transparent blur-2xl ${t.glow}`}
                />
                {/* sheen */}
                <div aria-hidden className="pointer-events-none absolute inset-0 overflow-hidden rounded-2xl">
                  <div
                    className="agls_sheen absolute inset-y-0 -left-1/3 w-1/3 -skew-x-12 bg-gradient-to-r from-transparent via-white/40 to-transparent opacity-0 group-hover:opacity-100 dark:via-white/15"
                    style={{ animation: prefersReduced ? "none" : "agls_sheen 1.4s ease-in-out" }}
                  />
                </div>

                <div className="relative flex items-start gap-3.5">
                  <span
                    aria-hidden
                    className="mt-0.5 flex h-9 w-9 shrink-0 items-center justify-center rounded-xl bg-white/70 ring-1 ring-white/60 dark:bg-white/10 dark:ring-white/10"
                  >
                    <span className="relative flex h-2.5 w-2.5">
                      <span className={`absolute inline-flex h-full w-full rounded-full opacity-60 ${t.dot} ${prefersReduced ? "" : "animate-ping"}`} />
                      <span className={`relative inline-flex h-2.5 w-2.5 rounded-full ${t.dot}`} />
                    </span>
                    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.7} className="absolute h-5 w-5 text-slate-700 opacity-0 transition-opacity group-hover:opacity-100 dark:text-slate-200">
                      {t.icon}
                    </svg>
                  </span>

                  <div className="min-w-0 flex-1">
                    <div className="flex flex-wrap items-center gap-2">
                      <span className={`inline-flex items-center rounded-full px-2 py-0.5 text-[11px] font-semibold uppercase tracking-wide ring-1 ${t.chip}`}>
                        {t.label}
                      </span>
                      <h3 className="truncate text-sm font-semibold text-slate-900 dark:text-white">
                        {a.title}
                      </h3>
                    </div>
                    <p className="mt-1 text-sm leading-relaxed text-slate-600 dark:text-slate-300">
                      {a.body}
                    </p>

                    <div className="mt-3 flex items-center gap-2">
                      <button
                        type="button"
                        onClick={() => act(a)}
                        className="rounded-lg bg-slate-900/90 px-3 py-1.5 text-xs font-semibold text-white shadow-sm transition hover:bg-slate-900 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-transparent dark:bg-white dark:text-slate-900 dark:hover:bg-slate-100"
                      >
                        {a.action}
                      </button>
                      <button
                        type="button"
                        onClick={() => dismiss(a.id)}
                        className="rounded-lg px-3 py-1.5 text-xs font-medium text-slate-600 transition hover:bg-white/70 hover:text-slate-900 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 dark:text-slate-300 dark:hover:bg-white/10 dark:hover:text-white"
                      >
                        Dismiss
                      </button>
                    </div>
                  </div>

                  <button
                    type="button"
                    onClick={() => dismiss(a.id)}
                    aria-label={`Dismiss: ${a.title}`}
                    className="-mr-1 -mt-1 flex h-8 w-8 shrink-0 items-center justify-center rounded-lg text-slate-500 transition hover:bg-white/70 hover:text-slate-900 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 dark:text-slate-400 dark:hover:bg-white/10 dark:hover:text-white"
                  >
                    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.8} className="h-4 w-4">
                      <path strokeLinecap="round" strokeLinejoin="round" d="M6 6l12 12M18 6L6 18" />
                    </svg>
                  </button>
                </div>
              </motion.article>
            );
          })}
        </AnimatePresence>

        {alerts.length === 0 && !resolved && (
          <motion.div
            initial={prefersReduced ? false : { opacity: 0, y: 8 }}
            animate={{ opacity: 1, y: 0 }}
            className="flex flex-col items-center gap-3 rounded-2xl border border-white/40 bg-white/40 px-6 py-12 text-center backdrop-blur-xl dark:border-white/10 dark:bg-white/5"
          >
            <span className="flex h-11 w-11 items-center justify-center rounded-full bg-emerald-500/15 text-emerald-600 ring-1 ring-emerald-500/25 dark:text-emerald-300">
              <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.8} className="h-5 w-5">
                <path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
              </svg>
            </span>
            <p className="text-sm font-medium text-slate-700 dark:text-slate-200">
              Inbox zero. Nothing needs you right now.
            </p>
            <button
              type="button"
              onClick={reset}
              className="rounded-full bg-slate-900/90 px-4 py-2 text-xs font-semibold text-white transition hover:bg-slate-900 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 dark:bg-white dark:text-slate-900 dark:hover:bg-slate-100"
            >
              Restore notifications
            </button>
          </motion.div>
        )}
      </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 →