Web InnoventixFreeCode

Card Newsletter

Original · free

newsletter card

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

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

type Status = "idle" | "loading" | "success" | "error";
type Cadence = "weekly" | "monthly";

interface Topic {
  id: string;
  label: string;
  hint: string;
}

const TOPICS: Topic[] = [
  { id: "systems", label: "Systems & Infra", hint: "Distributed systems, databases, ops" },
  { id: "ai", label: "AI / ML", hint: "Model tooling, evals, applied research" },
  { id: "webplatform", label: "Web Platform", hint: "Browsers, standards, rendering" },
  { id: "craft", label: "Engineering Craft", hint: "Reviews, architecture, career" },
];

const CADENCES: { id: Cadence; label: string; sub: string }[] = [
  { id: "weekly", label: "Weekly", sub: "Every Thursday" },
  { id: "monthly", label: "Monthly", sub: "First of the month" },
];

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

const EASE = [0.22, 1, 0.36, 1] as const;

export default function NewsxCard() {
  const reduce = useReducedMotion();
  const uid = useId();
  const emailId = `${uid}-email`;
  const errId = `${uid}-err`;
  const consentId = `${uid}-consent`;
  const topicsLabelId = `${uid}-topics`;

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

  const timerRef = useRef<number | null>(null);
  useEffect(() => {
    return () => {
      if (timerRef.current !== null) window.clearTimeout(timerRef.current);
    };
  }, []);

  function toggleTopic(id: string) {
    setTopics((prev) =>
      prev.includes(id) ? prev.filter((t) => t !== id) : [...prev, id]
    );
  }

  function handleSubmit(e: FormEvent<HTMLFormElement>) {
    e.preventDefault();
    if (status === "loading") return;

    const trimmed = email.trim();
    if (!EMAIL_RE.test(trimmed)) {
      setError("Enter a valid email address so we can reach you.");
      setStatus("error");
      return;
    }
    if (!consent) {
      setError("Please agree to receive the newsletter before subscribing.");
      setStatus("error");
      return;
    }

    setError(null);
    setStatus("loading");
    timerRef.current = window.setTimeout(() => {
      setSubmitted(trimmed);
      setStatus("success");
    }, 1100);
  }

  function reset() {
    setStatus("idle");
    setError(null);
    setEmail("");
    setConsent(false);
    setSubmitted("");
  }

  const motionProps = reduce
    ? {}
    : {
        initial: { opacity: 0, y: 10 },
        animate: { opacity: 1, y: 0 },
        exit: { opacity: 0, y: -10 },
        transition: { duration: 0.4, ease: EASE },
      };

  const isError = status === "error";
  const loading = status === "loading";

  return (
    <section className="relative w-full overflow-hidden bg-slate-50 px-4 py-16 sm:py-24 dark:bg-slate-950">
      <style>{`
        @keyframes newsx-drift {
          0%, 100% { transform: translate3d(0,0,0) scale(1); }
          50% { transform: translate3d(0,-14px,0) scale(1.06); }
        }
        @keyframes newsx-sheen {
          0% { transform: translateX(-120%); }
          100% { transform: translateX(220%); }
        }
        @keyframes newsx-blink {
          0%, 45% { opacity: 1; }
          55%, 100% { opacity: 0.25; }
        }
        .newsx-drift-a { animation: newsx-drift 11s ease-in-out infinite; }
        .newsx-drift-b { animation: newsx-drift 14s ease-in-out infinite reverse; }
        .newsx-blink { animation: newsx-blink 1.4s steps(1) infinite; }
        @media (prefers-reduced-motion: reduce) {
          .newsx-drift-a,
          .newsx-drift-b,
          .newsx-blink,
          .newsx-sheen { animation: none !important; }
        }
      `}</style>

      {/* Atmosphere */}
      <div aria-hidden className="pointer-events-none absolute inset-0">
        <div className="newsx-drift-a absolute -left-24 -top-24 h-72 w-72 rounded-full bg-indigo-300/40 blur-3xl dark:bg-indigo-600/20" />
        <div className="newsx-drift-b absolute -bottom-28 -right-16 h-80 w-80 rounded-full bg-violet-300/40 blur-3xl dark:bg-violet-700/20" />
      </div>

      <div className="relative mx-auto w-full max-w-xl">
        <article className="relative overflow-hidden rounded-3xl border border-slate-200 bg-white shadow-[0_20px_60px_-25px_rgba(30,27,75,0.35)] dark:border-slate-800 dark:bg-slate-900 dark:shadow-[0_20px_60px_-25px_rgba(0,0,0,0.8)]">
          {/* Top accent */}
          <div
            aria-hidden
            className="h-1.5 w-full bg-gradient-to-r from-indigo-500 via-violet-500 to-sky-400"
          />

          <div className="px-6 py-8 sm:px-9 sm:py-10">
            {/* Masthead */}
            <div className="flex items-start justify-between gap-4">
              <div>
                <span className="inline-flex items-center gap-2 rounded-full bg-indigo-50 px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.18em] text-indigo-700 ring-1 ring-inset ring-indigo-200 dark:bg-indigo-950/60 dark:text-indigo-300 dark:ring-indigo-800/70">
                  <span className="newsx-blink h-1.5 w-1.5 rounded-full bg-indigo-500 dark:bg-indigo-400" />
                  Issue 143 · Thursday
                </span>
                <h2 className="mt-4 font-serif text-3xl font-semibold tracking-tight text-slate-900 sm:text-4xl dark:text-white">
                  The Compile Step
                </h2>
              </div>
              <div
                aria-hidden
                className="grid h-12 w-12 shrink-0 place-items-center rounded-2xl bg-slate-900 text-white ring-1 ring-slate-900/10 dark:bg-white dark:text-slate-900 dark:ring-white/10"
              >
                <svg viewBox="0 0 24 24" fill="none" className="h-6 w-6" aria-hidden>
                  <path
                    d="M4 7.5 12 3l8 4.5v9L12 21l-8-4.5v-9Z"
                    stroke="currentColor"
                    strokeWidth="1.6"
                    strokeLinejoin="round"
                  />
                  <path
                    d="m8.5 11 2.2 2.2L15.5 8.5"
                    stroke="currentColor"
                    strokeWidth="1.6"
                    strokeLinecap="round"
                    strokeLinejoin="round"
                  />
                </svg>
              </div>
            </div>

            <p className="mt-4 max-w-md text-[15px] leading-relaxed text-slate-600 dark:text-slate-300">
              A no-fluff engineering dispatch. One deep teardown, three sharp
              links, and the tradeoffs behind them — written by people who ship.
            </p>

            {/* Stats */}
            <dl className="mt-6 grid grid-cols-3 divide-x divide-slate-200 rounded-2xl border border-slate-200 bg-slate-50/70 dark:divide-slate-800 dark:border-slate-800 dark:bg-slate-950/40">
              {[
                { k: "Readers", v: "24,180" },
                { k: "Open rate", v: "62%" },
                { k: "Read time", v: "6 min" },
              ].map((s) => (
                <div key={s.k} className="px-3 py-3 text-center">
                  <dd className="font-serif text-xl font-semibold text-slate-900 dark:text-white">
                    {s.v}
                  </dd>
                  <dt className="mt-0.5 text-[11px] font-medium uppercase tracking-wide text-slate-500 dark:text-slate-400">
                    {s.k}
                  </dt>
                </div>
              ))}
            </dl>

            <AnimatePresence mode="wait" initial={false}>
              {status === "success" ? (
                <motion.div key="done" {...motionProps} className="mt-8">
                  <div className="flex flex-col items-center rounded-2xl border border-emerald-200 bg-emerald-50 px-6 py-8 text-center dark:border-emerald-900/60 dark:bg-emerald-950/40">
                    <span className="grid h-14 w-14 place-items-center rounded-full bg-emerald-500 text-white shadow-lg shadow-emerald-500/30">
                      <svg viewBox="0 0 24 24" fill="none" className="h-7 w-7" aria-hidden>
                        <path
                          d="m5 12.5 4.2 4.2L19 7"
                          stroke="currentColor"
                          strokeWidth="2.2"
                          strokeLinecap="round"
                          strokeLinejoin="round"
                        />
                      </svg>
                    </span>
                    <h3 className="mt-4 font-serif text-2xl font-semibold text-slate-900 dark:text-white">
                      You&rsquo;re on the list
                    </h3>
                    <p className="mt-2 max-w-sm text-sm leading-relaxed text-slate-600 dark:text-slate-300">
                      A confirmation is on its way to{" "}
                      <span className="font-semibold text-emerald-700 dark:text-emerald-300">
                        {submitted}
                      </span>
                      . Your first {cadence} issue lands with the next send.
                    </p>
                    <button
                      type="button"
                      onClick={reset}
                      className="mt-6 rounded-full px-5 py-2 text-sm font-semibold text-emerald-700 underline-offset-4 hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-emerald-500 focus-visible:ring-offset-2 focus-visible:ring-offset-emerald-50 dark:text-emerald-300 dark:focus-visible:ring-offset-emerald-950"
                    >
                      Subscribe another email
                    </button>
                  </div>
                </motion.div>
              ) : (
                <motion.form
                  key="form"
                  {...motionProps}
                  onSubmit={handleSubmit}
                  noValidate
                  className="mt-8"
                >
                  {/* Topics */}
                  <fieldset>
                    <legend
                      id={topicsLabelId}
                      className="text-xs font-semibold uppercase tracking-wide text-slate-500 dark:text-slate-400"
                    >
                      Pick your tracks
                    </legend>
                    <div
                      role="group"
                      aria-labelledby={topicsLabelId}
                      className="mt-3 grid grid-cols-1 gap-2 sm:grid-cols-2"
                    >
                      {TOPICS.map((t) => {
                        const active = topics.includes(t.id);
                        return (
                          <button
                            key={t.id}
                            type="button"
                            aria-pressed={active}
                            onClick={() => toggleTopic(t.id)}
                            className={`group flex items-start gap-3 rounded-xl border px-3.5 py-3 text-left transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-slate-900 ${
                              active
                                ? "border-indigo-400 bg-indigo-50 dark:border-indigo-600 dark:bg-indigo-950/50"
                                : "border-slate-200 bg-white hover:border-slate-300 hover:bg-slate-50 dark:border-slate-800 dark:bg-slate-900 dark:hover:border-slate-700 dark:hover:bg-slate-800/50"
                            }`}
                          >
                            <span
                              aria-hidden
                              className={`mt-0.5 grid h-5 w-5 shrink-0 place-items-center rounded-md border transition-colors ${
                                active
                                  ? "border-indigo-500 bg-indigo-500 text-white"
                                  : "border-slate-300 bg-transparent text-transparent dark:border-slate-600"
                              }`}
                            >
                              <svg viewBox="0 0 24 24" fill="none" className="h-3.5 w-3.5" aria-hidden>
                                <path
                                  d="m5 12.5 4.2 4.2L19 7"
                                  stroke="currentColor"
                                  strokeWidth="2.6"
                                  strokeLinecap="round"
                                  strokeLinejoin="round"
                                />
                              </svg>
                            </span>
                            <span className="min-w-0">
                              <span className="block text-sm font-semibold text-slate-900 dark:text-white">
                                {t.label}
                              </span>
                              <span className="mt-0.5 block text-xs leading-snug text-slate-500 dark:text-slate-400">
                                {t.hint}
                              </span>
                            </span>
                          </button>
                        );
                      })}
                    </div>
                  </fieldset>

                  {/* Cadence */}
                  <fieldset className="mt-6">
                    <legend className="text-xs font-semibold uppercase tracking-wide text-slate-500 dark:text-slate-400">
                      Cadence
                    </legend>
                    <div
                      role="radiogroup"
                      aria-label="Delivery cadence"
                      className="mt-3 grid grid-cols-2 gap-2"
                    >
                      {CADENCES.map((c) => (
                        <label
                          key={c.id}
                          className="relative cursor-pointer"
                        >
                          <input
                            type="radio"
                            name={`${uid}-cadence`}
                            value={c.id}
                            checked={cadence === c.id}
                            onChange={() => setCadence(c.id)}
                            className="peer sr-only"
                          />
                          <span className="flex flex-col rounded-xl border border-slate-200 bg-white px-4 py-3 text-slate-700 transition-colors peer-checked:border-indigo-500 peer-checked:bg-indigo-50 peer-checked:text-indigo-900 peer-focus-visible:ring-2 peer-focus-visible:ring-indigo-500 peer-focus-visible:ring-offset-2 peer-focus-visible:ring-offset-white dark:border-slate-800 dark:bg-slate-900 dark:text-slate-300 dark:peer-checked:border-indigo-500 dark:peer-checked:bg-indigo-950/50 dark:peer-checked:text-indigo-100 dark:peer-focus-visible:ring-offset-slate-900">
                            <span className="text-sm font-semibold">{c.label}</span>
                            <span className="text-xs text-slate-500 peer-checked:text-indigo-700 dark:text-slate-400">
                              {c.sub}
                            </span>
                          </span>
                        </label>
                      ))}
                    </div>
                  </fieldset>

                  {/* Email */}
                  <div className="mt-6">
                    <label
                      htmlFor={emailId}
                      className="text-xs font-semibold uppercase tracking-wide text-slate-500 dark:text-slate-400"
                    >
                      Email address
                    </label>
                    <div className="mt-2 flex flex-col gap-2 sm:flex-row">
                      <div className="relative flex-1">
                        <span
                          aria-hidden
                          className="pointer-events-none absolute inset-y-0 left-3.5 flex items-center text-slate-400 dark:text-slate-500"
                        >
                          <svg viewBox="0 0 24 24" fill="none" className="h-5 w-5" aria-hidden>
                            <rect
                              x="3"
                              y="5"
                              width="18"
                              height="14"
                              rx="2.5"
                              stroke="currentColor"
                              strokeWidth="1.6"
                            />
                            <path
                              d="m4 7 8 6 8-6"
                              stroke="currentColor"
                              strokeWidth="1.6"
                              strokeLinecap="round"
                              strokeLinejoin="round"
                            />
                          </svg>
                        </span>
                        <input
                          id={emailId}
                          type="email"
                          inputMode="email"
                          autoComplete="email"
                          placeholder="you@yourdomain.dev"
                          value={email}
                          onChange={(e) => {
                            setEmail(e.target.value);
                            if (isError) {
                              setStatus("idle");
                              setError(null);
                            }
                          }}
                          aria-invalid={isError}
                          aria-describedby={error ? errId : undefined}
                          className={`w-full rounded-xl border bg-white py-3 pl-11 pr-3.5 text-[15px] text-slate-900 placeholder:text-slate-400 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:bg-slate-950 dark:text-white dark:placeholder:text-slate-500 dark:focus-visible:ring-offset-slate-900 ${
                            isError
                              ? "border-rose-400 focus-visible:ring-rose-500 dark:border-rose-700"
                              : "border-slate-300 focus-visible:ring-indigo-500 dark:border-slate-700"
                          }`}
                        />
                      </div>
                      <button
                        type="submit"
                        disabled={loading}
                        className="relative inline-flex shrink-0 items-center justify-center gap-2 overflow-hidden rounded-xl bg-slate-900 px-6 py-3 text-[15px] font-semibold text-white shadow-lg shadow-slate-900/20 transition-[transform,background-color] hover:bg-slate-800 active:scale-[0.98] 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:bg-white dark:text-slate-900 dark:shadow-white/10 dark:hover:bg-slate-100 dark:focus-visible:ring-offset-slate-900"
                      >
                        {!reduce && !loading && (
                          <span
                            aria-hidden
                            className="newsx-sheen absolute inset-y-0 left-0 w-1/3 -skew-x-12 bg-gradient-to-r from-transparent via-white/25 to-transparent"
                            style={{ animation: "newsx-sheen 2.8s ease-in-out infinite" }}
                          />
                        )}
                        {loading ? (
                          <>
                            <svg
                              viewBox="0 0 24 24"
                              fill="none"
                              className={`h-4 w-4 ${reduce ? "" : "animate-spin"}`}
                              aria-hidden
                            >
                              <circle
                                cx="12"
                                cy="12"
                                r="9"
                                stroke="currentColor"
                                strokeOpacity="0.25"
                                strokeWidth="2.5"
                              />
                              <path
                                d="M21 12a9 9 0 0 0-9-9"
                                stroke="currentColor"
                                strokeWidth="2.5"
                                strokeLinecap="round"
                              />
                            </svg>
                            <span className="relative">Joining…</span>
                          </>
                        ) : (
                          <span className="relative">Subscribe</span>
                        )}
                      </button>
                    </div>

                    <p
                      id={errId}
                      role="alert"
                      aria-live="assertive"
                      className={`mt-2 min-h-[1.1rem] text-sm font-medium text-rose-600 dark:text-rose-400 ${
                        error ? "" : "sr-only"
                      }`}
                    >
                      {error}
                    </p>
                  </div>

                  {/* Consent */}
                  <label
                    htmlFor={consentId}
                    className="mt-4 flex cursor-pointer items-start gap-3 text-sm text-slate-600 dark:text-slate-300"
                  >
                    <input
                      id={consentId}
                      type="checkbox"
                      checked={consent}
                      onChange={(e) => {
                        setConsent(e.target.checked);
                        if (isError) {
                          setStatus("idle");
                          setError(null);
                        }
                      }}
                      className="mt-0.5 h-4 w-4 shrink-0 rounded border-slate-300 text-indigo-600 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-600 dark:bg-slate-950 dark:focus-visible:ring-offset-slate-900"
                    />
                    <span className="leading-snug">
                      Send me The Compile Step and occasional product notes. No
                      spam, unsubscribe in one click.
                    </span>
                  </label>

                  <p className="mt-4 flex items-center gap-1.5 text-xs text-slate-400 dark:text-slate-500">
                    <svg viewBox="0 0 24 24" fill="none" className="h-3.5 w-3.5" aria-hidden>
                      <rect
                        x="5"
                        y="11"
                        width="14"
                        height="9"
                        rx="2"
                        stroke="currentColor"
                        strokeWidth="1.6"
                      />
                      <path
                        d="M8 11V8a4 4 0 1 1 8 0v3"
                        stroke="currentColor"
                        strokeWidth="1.6"
                        strokeLinecap="round"
                      />
                    </svg>
                    We store only your email. Read the privacy note.
                  </p>
                </motion.form>
              )}
            </AnimatePresence>
          </div>
        </article>
      </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 →