Web InnoventixFreeCode

Minimal Alert

Original · free

A minimal one-line alert with a dot indicator.

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

import { useState } from "react";

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

type MinimalAlert = {
  id: string;
  tone: Tone;
  message: string;
  action?: { label: string; href: string };
};

const TONES: Record<
  Tone,
  { dot: string; ring: string; label: string; text: string }
> = {
  info: {
    dot: "bg-sky-500",
    ring: "shadow-[0_0_0_4px_rgba(14,165,233,0.15)]",
    label: "Information",
    text: "text-slate-700 dark:text-slate-200",
  },
  success: {
    dot: "bg-emerald-500",
    ring: "shadow-[0_0_0_4px_rgba(16,185,129,0.15)]",
    label: "Success",
    text: "text-slate-700 dark:text-slate-200",
  },
  warning: {
    dot: "bg-amber-500",
    ring: "shadow-[0_0_0_4px_rgba(245,158,11,0.15)]",
    label: "Warning",
    text: "text-slate-700 dark:text-slate-200",
  },
  error: {
    dot: "bg-rose-500",
    ring: "shadow-[0_0_0_4px_rgba(244,63,94,0.15)]",
    label: "Error",
    text: "text-slate-700 dark:text-slate-200",
  },
};

const INITIAL: MinimalAlert[] = [
  {
    id: "deploy",
    tone: "success",
    message: "Deploy to production finished in 42s.",
    action: { label: "View log", href: "#log" },
  },
  {
    id: "seats",
    tone: "info",
    message: "3 new teammates joined your workspace this week.",
    action: { label: "Manage", href: "#team" },
  },
  {
    id: "quota",
    tone: "warning",
    message: "You've used 82% of your monthly API quota.",
    action: { label: "Upgrade", href: "#billing" },
  },
  {
    id: "webhook",
    tone: "error",
    message: "Webhook delivery to billing-service failed twice.",
    action: { label: "Retry", href: "#retry" },
  },
];

export default function AlertMinimal() {
  const [alerts, setAlerts] = useState<MinimalAlert[]>(INITIAL);

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

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

  return (
    <section className="relative w-full bg-slate-50 px-6 py-20 dark:bg-slate-950 sm:px-8">
      <style>{`
        @keyframes alertMinimal_in {
          from { opacity: 0; transform: translateY(-6px); }
          to { opacity: 1; transform: translateY(0); }
        }
        @keyframes alertMinimal_pulse {
          0%, 100% { transform: scale(1); opacity: 1; }
          50% { transform: scale(1.9); opacity: 0; }
        }
        .alertMinimal_row { animation: alertMinimal_in 0.35s cubic-bezier(0.22, 1, 0.36, 1) both; }
        .alertMinimal_ping { animation: alertMinimal_pulse 2s cubic-bezier(0, 0, 0.2, 1) infinite; }
        @media (prefers-reduced-motion: reduce) {
          .alertMinimal_row { animation: none; }
          .alertMinimal_ping { animation: none; }
        }
      `}</style>

      <div className="mx-auto w-full max-w-2xl">
        <div className="mb-8 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="mt-1 text-sm text-slate-500 dark:text-slate-400">
              Minimal one-line alerts with a status dot.
            </p>
          </div>
          {alerts.length < INITIAL.length && (
            <button
              type="button"
              onClick={reset}
              className="shrink-0 rounded-lg px-3 py-1.5 text-sm font-medium text-indigo-600 transition-colors hover:bg-indigo-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-slate-50 dark:text-indigo-400 dark:hover:bg-indigo-500/10 dark:focus-visible:ring-offset-slate-950"
            >
              Restore all
            </button>
          )}
        </div>

        <ul className="flex flex-col gap-2.5">
          {alerts.map((alert) => {
            const t = TONES[alert.tone];
            return (
              <li
                key={alert.id}
                role="status"
                aria-live="polite"
                className="alertMinimal_row group flex items-center gap-3 rounded-xl border border-slate-200 bg-white px-4 py-3 shadow-sm transition-shadow hover:shadow-md dark:border-slate-800 dark:bg-slate-900"
              >
                <span className="relative flex h-2.5 w-2.5 shrink-0">
                  <span
                    aria-hidden="true"
                    className={`alertMinimal_ping absolute inline-flex h-full w-full rounded-full ${t.dot} opacity-60`}
                  />
                  <span
                    className={`relative inline-flex h-2.5 w-2.5 rounded-full ${t.dot} ${t.ring}`}
                  />
                </span>

                <p
                  className={`min-w-0 flex-1 truncate text-sm font-medium ${t.text}`}
                >
                  <span className="sr-only">{t.label}: </span>
                  {alert.message}
                </p>

                {alert.action && (
                  <a
                    href={alert.action.href}
                    className="shrink-0 rounded-md px-2 py-1 text-sm font-semibold text-indigo-600 underline-offset-2 transition-colors hover:bg-indigo-50 hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 dark:text-indigo-400 dark:hover:bg-indigo-500/10"
                  >
                    {alert.action.label}
                  </a>
                )}

                <button
                  type="button"
                  onClick={() => dismiss(alert.id)}
                  aria-label={`Dismiss ${t.label.toLowerCase()} alert: ${alert.message}`}
                  className="shrink-0 rounded-md p-1 text-slate-400 opacity-60 transition-all hover:bg-slate-100 hover:text-slate-700 focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 group-hover:opacity-100 dark:hover:bg-slate-800 dark:hover:text-slate-200"
                >
                  <svg
                    viewBox="0 0 20 20"
                    fill="none"
                    className="h-4 w-4"
                    aria-hidden="true"
                  >
                    <path
                      d="M6 6l8 8M14 6l-8 8"
                      stroke="currentColor"
                      strokeWidth="1.75"
                      strokeLinecap="round"
                    />
                  </svg>
                </button>
              </li>
            );
          })}
        </ul>

        {alerts.length === 0 && (
          <div className="rounded-xl border border-dashed border-slate-300 bg-white px-4 py-10 text-center dark:border-slate-700 dark:bg-slate-900">
            <p className="text-sm font-medium text-slate-500 dark:text-slate-400">
              You're all caught up. No new notifications.
            </p>
          </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 →