Web InnoventixFreeCode

Success Newsletter

Original · free

newsletter with success state

byWeb InnoventixReact + Tailwind
newsxsuccessnewsletter
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/newsx-success.json
newsx-success.tsx
"use client";

import { type FormEvent, useCallback, useEffect, useId, useRef, useState } from "react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";

type Status = "idle" | "submitting" | "success";
type Cadence = "weekly" | "monthly";

interface Issue {
  no: string;
  title: string;
  read: string;
}

const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;

const ISSUES: Issue[] = [
  { no: "142", title: "The p99 latency trap nobody warns you about", read: "6 min" },
  { no: "141", title: "Postgres connection pooling, finally explained", read: "9 min" },
  { no: "140", title: "Why your feature flags are quietly becoming debt", read: "7 min" },
];

const CADENCE: { id: Cadence; label: string; hint: string }[] = [
  { id: "weekly", label: "Weekly digest", hint: "Every Friday, one deep-dive" },
  { id: "monthly", label: "Monthly roundup", hint: "First Monday, the best of the month" },
];

const CONFETTI = [
  { x: -132, y: -18, r: -24, d: 0, c: "#6366f1", s: 7 },
  { x: -96, y: -74, r: 40, d: 0.04, c: "#10b981", s: 9 },
  { x: -54, y: -108, r: -60, d: 0.09, c: "#f59e0b", s: 6 },
  { x: 0, y: -122, r: 12, d: 0.02, c: "#8b5cf6", s: 10 },
  { x: 54, y: -108, r: 72, d: 0.11, c: "#0ea5e9", s: 6 },
  { x: 96, y: -74, r: -36, d: 0.06, c: "#f43f5e", s: 8 },
  { x: 132, y: -18, r: 28, d: 0.03, c: "#10b981", s: 7 },
  { x: -70, y: 6, r: 54, d: 0.13, c: "#6366f1", s: 6 },
  { x: 70, y: 6, r: -48, d: 0.08, c: "#f59e0b", s: 9 },
] as const;

export default function NewsxSuccess() {
  const reduce = useReducedMotion();
  const uid = useId();
  const emailId = `${uid}-email`;
  const errorId = `${uid}-error`;
  const cadenceLabelId = `${uid}-cadence`;
  const liveId = `${uid}-live`;

  const [status, setStatus] = useState<Status>("idle");
  const [email, setEmail] = useState("");
  const [cadence, setCadence] = useState<Cadence>("weekly");
  const [error, setError] = useState<string | null>(null);
  const [submitted, setSubmitted] = useState("");

  const inputRef = useRef<HTMLInputElement>(null);
  const successRef = useRef<HTMLHeadingElement>(null);
  const timer = useRef<ReturnType<typeof setTimeout> | null>(null);

  useEffect(() => {
    return () => {
      if (timer.current) clearTimeout(timer.current);
    };
  }, []);

  useEffect(() => {
    if (status === "success") successRef.current?.focus();
  }, [status]);

  const handleSubmit = useCallback(
    (e: FormEvent<HTMLFormElement>) => {
      e.preventDefault();
      if (status === "submitting") return;
      const value = email.trim();
      if (value.length === 0) {
        setError("Enter your email so we know where to send it.");
        inputRef.current?.focus();
        return;
      }
      if (!EMAIL_RE.test(value)) {
        setError("That doesn't look like a valid email address.");
        inputRef.current?.focus();
        return;
      }
      setError(null);
      setStatus("submitting");
      timer.current = setTimeout(() => {
        setSubmitted(value);
        setStatus("success");
      }, 1100);
    },
    [email, status],
  );

  const handleReset = useCallback(() => {
    setStatus("idle");
    setError(null);
    setEmail("");
    setSubmitted("");
    requestAnimationFrame(() => inputRef.current?.focus());
  }, []);

  const cadenceCopy = CADENCE.find((c) => c.id === cadence)?.label ?? "Weekly digest";

  return (
    <section className="relative w-full overflow-hidden bg-slate-50 px-5 py-20 text-slate-900 sm:px-8 dark:bg-slate-950 dark:text-slate-100">
      <style>{`
        @keyframes newsx-float {
          0%, 100% { transform: translateY(0); }
          50% { transform: translateY(-6px); }
        }
        @keyframes newsx-shimmer {
          0% { background-position: 0% 50%; }
          100% { background-position: 200% 50%; }
        }
        @keyframes newsx-check {
          from { stroke-dashoffset: 34; }
          to { stroke-dashoffset: 0; }
        }
        @keyframes newsx-ring {
          0% { transform: scale(0.6); opacity: 0.8; }
          100% { transform: scale(1.9); opacity: 0; }
        }
        .newsx-float { animation: newsx-float 5s ease-in-out infinite; }
        .newsx-shimmer {
          background-size: 200% 100%;
          animation: newsx-shimmer 3.5s linear infinite;
        }
        .newsx-check-path {
          stroke-dasharray: 34;
          stroke-dashoffset: 34;
          animation: newsx-check 0.5s 0.15s ease-out forwards;
        }
        .newsx-ring { animation: newsx-ring 1.1s 0.1s ease-out forwards; }
        @media (prefers-reduced-motion: reduce) {
          .newsx-float,
          .newsx-shimmer,
          .newsx-check-path,
          .newsx-ring {
            animation: none !important;
          }
          .newsx-check-path { stroke-dashoffset: 0; }
        }
      `}</style>

      <div
        aria-hidden="true"
        className="pointer-events-none absolute -top-24 left-1/2 h-72 w-[42rem] max-w-full -translate-x-1/2 rounded-full bg-gradient-to-r from-indigo-300/40 via-violet-300/30 to-sky-300/40 blur-3xl dark:from-indigo-600/20 dark:via-violet-600/15 dark:to-sky-600/20"
      />

      <div className="relative mx-auto grid w-full max-w-5xl items-center gap-12 lg:grid-cols-[1.05fr_1fr] lg:gap-16">
        {/* Left: pitch */}
        <div className="max-w-xl">
          <span className="inline-flex items-center gap-2 rounded-full border border-slate-200 bg-white/70 px-3 py-1 text-xs font-semibold uppercase tracking-wider text-indigo-600 backdrop-blur dark:border-slate-800 dark:bg-slate-900/70 dark:text-indigo-300">
            <span className="newsx-float relative flex h-2 w-2">
              <span className="absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75" />
              <span className="relative inline-flex h-2 w-2 rounded-full bg-emerald-500" />
            </span>
            Issue 142 · shipping now
          </span>

          <h2 className="mt-5 text-3xl font-bold tracking-tight sm:text-4xl">
            <span className="bg-gradient-to-r from-indigo-600 via-violet-600 to-sky-500 bg-clip-text text-transparent dark:from-indigo-300 dark:via-violet-300 dark:to-sky-300">
              Deploy Friday
            </span>
          </h2>
          <p className="mt-3 text-base leading-relaxed text-slate-600 dark:text-slate-300">
            A weekly letter for backend engineers who actually ship. One hard-won
            production lesson, no fluff, no sponsor detours. Read in six minutes,
            think about it all weekend.
          </p>

          <ul className="mt-8 space-y-1.5" aria-label="Recent issues">
            {ISSUES.map((issue) => (
              <li key={issue.no}>
                <a
                  href="#"
                  className="group flex items-center gap-3 rounded-xl border border-transparent px-3 py-2.5 transition-colors hover:border-slate-200 hover:bg-white focus-visible:border-slate-200 focus-visible:bg-white 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:hover:border-slate-800 dark:hover:bg-slate-900 dark:focus-visible:border-slate-800 dark:focus-visible:bg-slate-900 dark:focus-visible:ring-offset-slate-950"
                >
                  <span className="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-slate-100 text-xs font-bold tabular-nums text-slate-500 transition-colors group-hover:bg-indigo-50 group-hover:text-indigo-600 dark:bg-slate-800 dark:text-slate-400 dark:group-hover:bg-indigo-500/15 dark:group-hover:text-indigo-300">
                    #{issue.no}
                  </span>
                  <span className="min-w-0 flex-1">
                    <span className="block truncate text-sm font-medium text-slate-800 dark:text-slate-100">
                      {issue.title}
                    </span>
                    <span className="text-xs text-slate-400 dark:text-slate-500">
                      {issue.read} read
                    </span>
                  </span>
                  <svg
                    aria-hidden="true"
                    viewBox="0 0 24 24"
                    className="h-4 w-4 shrink-0 text-slate-300 transition-transform group-hover:translate-x-0.5 group-hover:text-indigo-500 dark:text-slate-600"
                    fill="none"
                    stroke="currentColor"
                    strokeWidth={2}
                    strokeLinecap="round"
                    strokeLinejoin="round"
                  >
                    <path d="M5 12h14M13 6l6 6-6 6" />
                  </svg>
                </a>
              </li>
            ))}
          </ul>

          <div className="mt-8 flex items-center gap-3">
            <div className="flex -space-x-2" aria-hidden="true">
              {["#6366f1", "#10b981", "#f59e0b", "#0ea5e9"].map((c, i) => (
                <span
                  key={c}
                  className="flex h-8 w-8 items-center justify-center rounded-full border-2 border-slate-50 text-[11px] font-bold text-white dark:border-slate-950"
                  style={{ backgroundColor: c }}
                >
                  {["JR", "MK", "AL", "TS"][i]}
                </span>
              ))}
            </div>
            <p className="text-sm text-slate-500 dark:text-slate-400">
              Joined by{" "}
              <span className="font-semibold text-slate-700 dark:text-slate-200">
                12,480
              </span>{" "}
              engineers
            </p>
          </div>
        </div>

        {/* Right: card */}
        <div className="relative">
          <div className="relative overflow-hidden rounded-2xl border border-slate-200 bg-white p-6 shadow-xl shadow-slate-900/5 sm:p-8 dark:border-slate-800 dark:bg-slate-900 dark:shadow-black/40">
            <div
              aria-hidden="true"
              className="newsx-shimmer absolute inset-x-0 top-0 h-px bg-gradient-to-r from-transparent via-indigo-500 to-transparent"
            />

            {/* polite live region for status changes */}
            <p id={liveId} className="sr-only" role="status" aria-live="polite">
              {status === "submitting"
                ? "Submitting your subscription."
                : status === "success"
                  ? `Subscribed. A confirmation link was sent to ${submitted}.`
                  : ""}
            </p>

            <AnimatePresence mode="wait" initial={false}>
              {status === "success" ? (
                <motion.div
                  key="success"
                  initial={reduce ? false : { opacity: 0, y: 8 }}
                  animate={{ opacity: 1, y: 0 }}
                  exit={reduce ? undefined : { opacity: 0, y: -8 }}
                  transition={{ duration: 0.3, ease: "easeOut" }}
                  className="flex flex-col items-center py-2 text-center"
                >
                  <div className="relative mb-5 flex h-16 w-16 items-center justify-center">
                    {!reduce && (
                      <span
                        aria-hidden="true"
                        className="newsx-ring absolute inset-0 rounded-full bg-emerald-400/40"
                      />
                    )}
                    {!reduce &&
                      CONFETTI.map((p, i) => (
                        <motion.span
                          key={i}
                          aria-hidden="true"
                          className="absolute rounded-[2px]"
                          style={{
                            width: p.s,
                            height: p.s,
                            backgroundColor: p.c,
                            rotate: `${p.r}deg`,
                          }}
                          initial={{ x: 0, y: 0, scale: 0, opacity: 1 }}
                          animate={{ x: p.x, y: p.y, scale: 1, opacity: 0 }}
                          transition={{
                            duration: 0.9,
                            delay: 0.12 + p.d,
                            ease: "easeOut",
                          }}
                        />
                      ))}
                    <span className="relative flex h-16 w-16 items-center justify-center rounded-full bg-emerald-500 text-white shadow-lg shadow-emerald-500/30">
                      <svg
                        aria-hidden="true"
                        viewBox="0 0 24 24"
                        className="h-8 w-8"
                        fill="none"
                        stroke="currentColor"
                        strokeWidth={3}
                        strokeLinecap="round"
                        strokeLinejoin="round"
                      >
                        <path className="newsx-check-path" d="M5 13l4 4L19 7" />
                      </svg>
                    </span>
                  </div>

                  <h3
                    ref={successRef}
                    tabIndex={-1}
                    className="text-xl font-bold tracking-tight text-slate-900 outline-none sm:text-2xl dark:text-slate-50"
                  >
                    You&apos;re on the list.
                  </h3>
                  <p className="mt-2 max-w-xs text-sm leading-relaxed text-slate-600 dark:text-slate-300">
                    We sent a confirmation link to{" "}
                    <span className="font-semibold text-slate-900 dark:text-slate-100">
                      {submitted}
                    </span>
                    . Click it to lock in your{" "}
                    <span className="whitespace-nowrap">{cadenceCopy.toLowerCase()}</span>.
                  </p>

                  <div className="mt-6 flex w-full flex-col gap-3 sm:flex-row sm:justify-center">
                    <a
                      href="#"
                      className="inline-flex items-center justify-center gap-2 rounded-xl bg-slate-900 px-4 py-2.5 text-sm font-semibold text-white transition-colors hover:bg-slate-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:bg-white dark:text-slate-900 dark:hover:bg-slate-200 dark:focus-visible:ring-offset-slate-900"
                    >
                      Read issue #142
                      <svg
                        aria-hidden="true"
                        viewBox="0 0 24 24"
                        className="h-4 w-4"
                        fill="none"
                        stroke="currentColor"
                        strokeWidth={2}
                        strokeLinecap="round"
                        strokeLinejoin="round"
                      >
                        <path d="M5 12h14M13 6l6 6-6 6" />
                      </svg>
                    </a>
                    <button
                      type="button"
                      onClick={handleReset}
                      className="inline-flex items-center justify-center rounded-xl border border-slate-200 px-4 py-2.5 text-sm font-semibold text-slate-600 transition-colors hover:bg-slate-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:border-slate-700 dark:text-slate-300 dark:hover:bg-slate-800 dark:focus-visible:ring-offset-slate-900"
                    >
                      Use a different email
                    </button>
                  </div>
                </motion.div>
              ) : (
                <motion.div
                  key="form"
                  initial={reduce ? false : { opacity: 0, y: 8 }}
                  animate={{ opacity: 1, y: 0 }}
                  exit={reduce ? undefined : { opacity: 0, y: -8 }}
                  transition={{ duration: 0.3, ease: "easeOut" }}
                >
                  <h3 className="text-xl font-bold tracking-tight sm:text-2xl">
                    Get the next issue in your inbox
                  </h3>
                  <p className="mt-1.5 text-sm text-slate-500 dark:text-slate-400">
                    Free forever. Unsubscribe in one click, no hard feelings.
                  </p>

                  <form onSubmit={handleSubmit} noValidate className="mt-6">
                    <label
                      htmlFor={emailId}
                      className="block text-sm font-medium text-slate-700 dark:text-slate-200"
                    >
                      Email address
                    </label>
                    <div className="relative mt-1.5">
                      <span
                        aria-hidden="true"
                        className="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3.5 text-slate-400 dark:text-slate-500"
                      >
                        <svg
                          viewBox="0 0 24 24"
                          className="h-5 w-5"
                          fill="none"
                          stroke="currentColor"
                          strokeWidth={1.8}
                          strokeLinecap="round"
                          strokeLinejoin="round"
                        >
                          <rect x="3" y="5" width="18" height="14" rx="2" />
                          <path d="M3 7l9 6 9-6" />
                        </svg>
                      </span>
                      <input
                        ref={inputRef}
                        id={emailId}
                        name="email"
                        type="email"
                        inputMode="email"
                        autoComplete="email"
                        placeholder="you@company.dev"
                        value={email}
                        disabled={status === "submitting"}
                        aria-invalid={error ? true : undefined}
                        aria-describedby={error ? errorId : undefined}
                        onChange={(e) => {
                          setEmail(e.target.value);
                          if (error) setError(null);
                        }}
                        className={`w-full rounded-xl border bg-white py-3 pl-11 pr-3 text-sm text-slate-900 placeholder:text-slate-400 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-offset-white disabled:opacity-60 dark:bg-slate-950 dark:text-slate-50 dark:placeholder:text-slate-500 dark:focus-visible:ring-offset-slate-900 ${
                          error
                            ? "border-rose-400 focus-visible:ring-rose-500 dark:border-rose-500/70"
                            : "border-slate-300 focus-visible:ring-indigo-500 dark:border-slate-700"
                        }`}
                      />
                    </div>

                    <AnimatePresence initial={false}>
                      {error && (
                        <motion.p
                          id={errorId}
                          role="alert"
                          initial={reduce ? false : { opacity: 0, height: 0 }}
                          animate={{ opacity: 1, height: "auto" }}
                          exit={reduce ? undefined : { opacity: 0, height: 0 }}
                          transition={{ duration: 0.2 }}
                          className="flex items-center gap-1.5 overflow-hidden pt-2 text-sm text-rose-600 dark:text-rose-400"
                        >
                          <svg
                            aria-hidden="true"
                            viewBox="0 0 24 24"
                            className="h-4 w-4 shrink-0"
                            fill="none"
                            stroke="currentColor"
                            strokeWidth={2}
                            strokeLinecap="round"
                            strokeLinejoin="round"
                          >
                            <circle cx="12" cy="12" r="9" />
                            <path d="M12 8v4M12 16h.01" />
                          </svg>
                          {error}
                        </motion.p>
                      )}
                    </AnimatePresence>

                    <fieldset className="mt-5">
                      <legend
                        id={cadenceLabelId}
                        className="text-sm font-medium text-slate-700 dark:text-slate-200"
                      >
                        How often?
                      </legend>
                      <div
                        role="radiogroup"
                        aria-labelledby={cadenceLabelId}
                        className="mt-2 grid grid-cols-1 gap-2 sm:grid-cols-2"
                      >
                        {CADENCE.map((c) => {
                          const active = cadence === c.id;
                          return (
                            <label
                              key={c.id}
                              className={`relative flex cursor-pointer flex-col rounded-xl border p-3 transition-colors focus-within:ring-2 focus-within:ring-indigo-500 focus-within:ring-offset-2 focus-within:ring-offset-white dark:focus-within:ring-offset-slate-900 ${
                                active
                                  ? "border-indigo-500 bg-indigo-50/70 dark:border-indigo-400/70 dark:bg-indigo-500/10"
                                  : "border-slate-200 hover:border-slate-300 dark:border-slate-700 dark:hover:border-slate-600"
                              }`}
                            >
                              <input
                                type="radio"
                                name={`${uid}-cadence`}
                                value={c.id}
                                checked={active}
                                disabled={status === "submitting"}
                                onChange={() => setCadence(c.id)}
                                className="sr-only"
                              />
                              <span className="flex items-center justify-between">
                                <span className="text-sm font-semibold text-slate-800 dark:text-slate-100">
                                  {c.label}
                                </span>
                                <span
                                  aria-hidden="true"
                                  className={`flex h-4 w-4 items-center justify-center rounded-full border transition-colors ${
                                    active
                                      ? "border-indigo-500 bg-indigo-500 dark:border-indigo-400 dark:bg-indigo-400"
                                      : "border-slate-300 dark:border-slate-600"
                                  }`}
                                >
                                  {active && (
                                    <span className="h-1.5 w-1.5 rounded-full bg-white" />
                                  )}
                                </span>
                              </span>
                              <span className="mt-0.5 text-xs text-slate-500 dark:text-slate-400">
                                {c.hint}
                              </span>
                            </label>
                          );
                        })}
                      </div>
                    </fieldset>

                    <button
                      type="submit"
                      disabled={status === "submitting"}
                      className="group mt-6 flex w-full items-center justify-center gap-2 rounded-xl bg-gradient-to-r from-indigo-600 to-violet-600 px-4 py-3 text-sm font-semibold text-white shadow-lg shadow-indigo-600/25 transition-all hover:from-indigo-500 hover:to-violet-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white disabled:cursor-not-allowed disabled:opacity-80 dark:focus-visible:ring-offset-slate-900"
                    >
                      {status === "submitting" ? (
                        <>
                          <svg
                            aria-hidden="true"
                            viewBox="0 0 24 24"
                            className="h-4 w-4 animate-spin"
                            fill="none"
                          >
                            <circle
                              cx="12"
                              cy="12"
                              r="9"
                              stroke="currentColor"
                              strokeWidth={3}
                              className="opacity-25"
                            />
                            <path
                              d="M21 12a9 9 0 0 0-9-9"
                              stroke="currentColor"
                              strokeWidth={3}
                              strokeLinecap="round"
                            />
                          </svg>
                          Subscribing…
                        </>
                      ) : (
                        <>
                          Subscribe
                          <svg
                            aria-hidden="true"
                            viewBox="0 0 24 24"
                            className="h-4 w-4 transition-transform group-hover:translate-x-0.5"
                            fill="none"
                            stroke="currentColor"
                            strokeWidth={2}
                            strokeLinecap="round"
                            strokeLinejoin="round"
                          >
                            <path d="M5 12h14M13 6l6 6-6 6" />
                          </svg>
                        </>
                      )}
                    </button>

                    <p className="mt-3 text-center text-xs text-slate-400 dark:text-slate-500">
                      No spam. We never share your address.
                    </p>
                  </form>
                </motion.div>
              )}
            </AnimatePresence>
          </div>
        </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 →