Web InnoventixFreeCode

Benefits Newsletter

Original · free

newsletter with benefit bullets

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

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

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

interface Benefit {
  title: string;
  body: string;
  icon: ReactNode;
}

const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/;

const BENEFITS: Benefit[] = [
  {
    title: "One deep teardown every Sunday",
    body: "A real shipping interface pulled apart to the pixel — type ramp, spacing logic, and the motion curves behind it.",
    icon: (
      <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.6} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" className="h-5 w-5">
        <path d="M10.5 3.5h-6a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1v-6a1 1 0 0 0-1-1Z" />
        <path d="M20.5 12.5h-6a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1v-6a1 1 0 0 0-1-1Z" />
        <path d="M13.5 3.5h6a1 1 0 0 1 1 1v3.5" />
        <path d="M10.5 20.5h-6a1 1 0 0 1-1-1V16" />
      </svg>
    ),
  },
  {
    title: "Copy-paste recipes, not theory",
    body: "The exact CSS, easing curves, and layout math — dropped in a block you can lift straight into your next build.",
    icon: (
      <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.6} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" className="h-5 w-5">
        <path d="m8 7-4.5 5L8 17" />
        <path d="m16 7 4.5 5L16 17" />
        <path d="m13.5 4-3 16" />
      </svg>
    ),
  },
  {
    title: "No sponsors, no tracking pixels",
    body: "The letter is funded by readers, so nothing is sold to you and nothing is watching you read. Ever.",
    icon: (
      <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.6} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" className="h-5 w-5">
        <path d="M12 3 4.5 6v5.5c0 4.4 3.1 7.6 7.5 9 4.4-1.4 7.5-4.6 7.5-9V6L12 3Z" />
        <path d="m9 12 2 2 4-4.5" />
      </svg>
    ),
  },
  {
    title: "141 issues, fully searchable",
    body: "The whole archive is open and free — years of teardowns indexed by pattern, component, and technique.",
    icon: (
      <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.6} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" className="h-5 w-5">
        <path d="M3.5 7.5 12 4l8.5 3.5L12 11 3.5 7.5Z" />
        <path d="m3.5 12 8.5 3.5L20.5 12" />
        <path d="m3.5 16.5 8.5 3.5 8.5-3.5" />
      </svg>
    ),
  },
];

const listContainer: Variants = {
  hidden: {},
  show: { transition: { staggerChildren: 0.09, delayChildren: 0.12 } },
};

const listItem: Variants = {
  hidden: { opacity: 0, y: 18 },
  show: { opacity: 1, y: 0, transition: { duration: 0.5, ease: "easeOut" } },
};

export default function NewsxBenefits() {
  const reduce = useReducedMotion();
  const uid = useId();
  const [email, setEmail] = useState("");
  const [cadence, setCadence] = useState<Cadence>("weekly");
  const [status, setStatus] = useState<Status>("idle");
  const [error, setError] = useState<string | null>(null);
  const timer = useRef<number | null>(null);

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

  function onEmailChange(e: ChangeEvent<HTMLInputElement>) {
    setEmail(e.target.value);
    if (error) setError(null);
  }

  function onSubmit(e: FormEvent<HTMLFormElement>) {
    e.preventDefault();
    if (status === "loading") return;
    const value = email.trim();
    if (!EMAIL_RE.test(value)) {
      setError("Enter a valid email so we know where to send it.");
      return;
    }
    setError(null);
    setStatus("loading");
    timer.current = window.setTimeout(() => setStatus("success"), 1150);
  }

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

  const emailId = `${uid}-email`;
  const errId = `${uid}-email-error`;
  const cadenceName = `${uid}-cadence`;
  const focusRing =
    "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-amber-500 focus-visible:ring-offset-2 focus-visible:ring-offset-neutral-50 dark:focus-visible:ring-offset-neutral-950";

  return (
    <section className="relative w-full overflow-hidden bg-gradient-to-b from-neutral-50 to-neutral-100 text-neutral-900 dark:from-neutral-950 dark:to-neutral-900 dark:text-neutral-100">
      <style>{`
        @keyframes nxb-pulse { 0%, 100% { opacity: 1; transform: scale(1); } 50% { opacity: .5; transform: scale(.78); } }
        @keyframes nxb-float { 0%, 100% { transform: translateY(0) rotate(-6deg); } 50% { transform: translateY(-7px) rotate(-6deg); } }
        @keyframes nxb-spin { to { transform: rotate(360deg); } }
        .nxb-dot { animation: nxb-pulse 2.4s ease-in-out infinite; }
        .nxb-stamp { animation: nxb-float 6s ease-in-out infinite; }
        .nxb-spin { animation: nxb-spin .7s linear infinite; }
        @media (prefers-reduced-motion: reduce) {
          .nxb-dot, .nxb-stamp, .nxb-spin { animation: none !important; }
        }
      `}</style>

      {/* atmosphere */}
      <div aria-hidden="true" className="pointer-events-none absolute inset-0">
        <div className="absolute -left-24 -top-24 h-72 w-72 rounded-full bg-amber-300/40 blur-3xl dark:bg-amber-500/10" />
        <div className="absolute -right-16 top-1/3 h-80 w-80 rounded-full bg-indigo-300/30 blur-3xl dark:bg-indigo-500/10" />
        <div className="absolute inset-x-0 top-0 h-px bg-gradient-to-r from-transparent via-neutral-300 to-transparent dark:via-neutral-700" />
      </div>

      <div className="relative mx-auto max-w-6xl px-6 py-20 sm:py-28">
        <div className="grid grid-cols-1 gap-14 lg:grid-cols-[1.05fr_0.95fr] lg:gap-16">
          {/* ── Left: the pitch ── */}
          <div>
            <p className="text-xs font-semibold uppercase tracking-[0.35em] text-amber-700 dark:text-amber-400">
              Frontmatter
            </p>
            <p className="mt-4 inline-flex items-center gap-2 rounded-full border border-neutral-300/80 bg-white/60 px-3 py-1 text-xs font-medium text-neutral-600 backdrop-blur dark:border-neutral-700/80 dark:bg-neutral-900/60 dark:text-neutral-300">
              <span className="nxb-dot inline-block h-1.5 w-1.5 rounded-full bg-emerald-500" />
              Issue 141 ships this Sunday
            </p>

            <h2 className="mt-6 font-serif text-4xl leading-[1.05] tracking-tight text-neutral-900 sm:text-5xl dark:text-neutral-50">
              Interface craft,
              <br />
              <span className="italic text-amber-700 dark:text-amber-400">one teardown</span> at a time.
            </h2>

            <p className="mt-6 max-w-xl text-base leading-relaxed text-neutral-600 dark:text-neutral-400">
              Every Sunday, one real product interface reverse-engineered to the
              pixel — the type ramp, the motion, the spacing logic — with recipes
              you can paste straight into your next build. Written for people who
              sweat the details nobody else notices.
            </p>

            <motion.ul
              variants={reduce ? undefined : listContainer}
              initial={reduce ? false : "hidden"}
              whileInView={reduce ? undefined : "show"}
              viewport={{ once: true, margin: "-12% 0px" }}
              className="mt-10 grid grid-cols-1 gap-x-8 gap-y-6 sm:grid-cols-2"
            >
              {BENEFITS.map((b) => (
                <motion.li
                  key={b.title}
                  variants={reduce ? undefined : listItem}
                  className="flex gap-3.5"
                >
                  <span className="mt-0.5 flex h-9 w-9 shrink-0 items-center justify-center rounded-xl border border-amber-200 bg-amber-50 text-amber-700 dark:border-amber-500/30 dark:bg-amber-500/10 dark:text-amber-400">
                    {b.icon}
                  </span>
                  <span>
                    <span className="block text-sm font-semibold text-neutral-900 dark:text-neutral-100">
                      {b.title}
                    </span>
                    <span className="mt-1 block text-sm leading-relaxed text-neutral-600 dark:text-neutral-400">
                      {b.body}
                    </span>
                  </span>
                </motion.li>
              ))}
            </motion.ul>
          </div>

          {/* ── Right: the signup card ── */}
          <div className="relative">
            <span
              aria-hidden="true"
              className="nxb-stamp absolute -right-3 -top-4 z-10 flex h-16 w-16 -rotate-6 flex-col items-center justify-center rounded-full border-2 border-dashed border-amber-500/70 bg-amber-50 text-amber-800 shadow-sm dark:bg-neutral-900 dark:text-amber-300"
            >
              <span className="text-[9px] font-semibold uppercase tracking-widest">Issue</span>
              <span className="font-serif text-lg leading-none">141</span>
            </span>

            <div className="relative rounded-3xl border border-neutral-200 bg-white/80 p-7 shadow-xl shadow-neutral-900/5 backdrop-blur-sm sm:p-9 dark:border-neutral-800 dark:bg-neutral-900/70 dark:shadow-black/30">
              <AnimatePresence mode="wait" initial={false}>
                {status === "success" ? (
                  <motion.div
                    key="done"
                    initial={reduce ? false : { opacity: 0, y: 12 }}
                    animate={reduce ? undefined : { opacity: 1, y: 0 }}
                    exit={reduce ? undefined : { opacity: 0, y: -8 }}
                    transition={{ duration: 0.4, ease: "easeOut" }}
                  >
                    <span className="flex h-12 w-12 items-center justify-center rounded-2xl bg-emerald-500/15 text-emerald-600 dark:text-emerald-400">
                      <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" className="h-6 w-6">
                        <path d="m5 12.5 4.2 4L19 7" />
                      </svg>
                    </span>
                    <h3 className="mt-5 font-serif text-2xl text-neutral-900 dark:text-neutral-50">
                      You&rsquo;re on the list.
                    </h3>
                    <p className="mt-3 text-sm leading-relaxed text-neutral-600 dark:text-neutral-400">
                      The next issue lands in{" "}
                      <span className="font-medium text-neutral-900 dark:text-neutral-100">
                        {email}
                      </span>{" "}
                      {cadence === "weekly" ? "every Sunday" : "on the first of each month"}.
                      Check your inbox for a one-tap confirmation to lock it in.
                    </p>
                    <button
                      type="button"
                      onClick={reset}
                      className={`mt-6 text-sm font-semibold text-amber-700 underline decoration-amber-400/50 underline-offset-4 transition hover:decoration-amber-500 dark:text-amber-400 ${focusRing} rounded`}
                    >
                      Use a different email
                    </button>
                  </motion.div>
                ) : (
                  <motion.form
                    key="form"
                    onSubmit={onSubmit}
                    noValidate
                    initial={reduce ? false : { opacity: 0, y: 12 }}
                    animate={reduce ? undefined : { opacity: 1, y: 0 }}
                    exit={reduce ? undefined : { opacity: 0, y: -8 }}
                    transition={{ duration: 0.4, ease: "easeOut" }}
                  >
                    <h3 className="font-serif text-2xl text-neutral-900 dark:text-neutral-50">
                      Get the next issue
                    </h3>
                    <p className="mt-2 text-sm text-neutral-600 dark:text-neutral-400">
                      Free forever. One click to leave, no hard feelings.
                    </p>

                    <div className="mt-6">
                      <label
                        htmlFor={emailId}
                        className="block text-xs font-semibold uppercase tracking-wide text-neutral-500 dark:text-neutral-400"
                      >
                        Email address
                      </label>
                      <input
                        id={emailId}
                        type="email"
                        inputMode="email"
                        autoComplete="email"
                        placeholder="you@studio.com"
                        value={email}
                        onChange={onEmailChange}
                        aria-invalid={error ? true : undefined}
                        aria-describedby={error ? errId : undefined}
                        className={`mt-2 w-full rounded-xl border bg-white px-4 py-3 text-sm text-neutral-900 placeholder:text-neutral-400 transition dark:bg-neutral-950 dark:text-neutral-100 dark:placeholder:text-neutral-500 ${focusRing} ${
                          error
                            ? "border-rose-400 dark:border-rose-500/70"
                            : "border-neutral-300 hover:border-neutral-400 dark:border-neutral-700 dark:hover:border-neutral-600"
                        }`}
                      />
                      {error ? (
                        <p
                          id={errId}
                          role="alert"
                          className="mt-2 flex items-center gap-1.5 text-sm text-rose-600 dark:text-rose-400"
                        >
                          <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.8} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" className="h-4 w-4">
                            <circle cx="12" cy="12" r="9" />
                            <path d="M12 7.5v5M12 16h.01" />
                          </svg>
                          {error}
                        </p>
                      ) : null}
                    </div>

                    <fieldset className="mt-5">
                      <legend className="text-xs font-semibold uppercase tracking-wide text-neutral-500 dark:text-neutral-400">
                        How often?
                      </legend>
                      <div className="mt-2 grid grid-cols-2 gap-2">
                        {([
                          { value: "weekly", label: "Every issue", note: "Weekly" },
                          { value: "monthly", label: "Monthly digest", note: "12 / year" },
                        ] as const).map((opt) => (
                          <label key={opt.value} className="relative cursor-pointer">
                            <input
                              type="radio"
                              name={cadenceName}
                              value={opt.value}
                              checked={cadence === opt.value}
                              onChange={() => setCadence(opt.value)}
                              className="peer sr-only"
                            />
                            <span className="flex flex-col rounded-xl border border-neutral-300 bg-white px-3.5 py-2.5 text-left transition peer-checked:border-amber-500 peer-checked:bg-amber-50 peer-focus-visible:ring-2 peer-focus-visible:ring-amber-500 peer-focus-visible:ring-offset-2 peer-focus-visible:ring-offset-white dark:border-neutral-700 dark:bg-neutral-950 dark:peer-checked:border-amber-500 dark:peer-checked:bg-amber-500/10 dark:peer-focus-visible:ring-offset-neutral-900">
                              <span className="text-sm font-semibold text-neutral-900 dark:text-neutral-100">
                                {opt.label}
                              </span>
                              <span className="text-xs text-neutral-500 dark:text-neutral-400">
                                {opt.note}
                              </span>
                            </span>
                          </label>
                        ))}
                      </div>
                    </fieldset>

                    <button
                      type="submit"
                      disabled={status === "loading"}
                      className={`mt-6 flex w-full items-center justify-center gap-2 rounded-xl bg-neutral-900 px-5 py-3 text-sm font-semibold text-white transition hover:bg-neutral-800 disabled:cursor-not-allowed disabled:opacity-70 dark:bg-amber-400 dark:text-neutral-950 dark:hover:bg-amber-300 ${focusRing}`}
                    >
                      {status === "loading" ? (
                        <>
                          <svg viewBox="0 0 24 24" fill="none" aria-hidden="true" className="nxb-spin h-4 w-4">
                            <circle cx="12" cy="12" r="9" stroke="currentColor" strokeWidth="3" opacity="0.25" />
                            <path d="M12 3a9 9 0 0 1 9 9" stroke="currentColor" strokeWidth="3" strokeLinecap="round" />
                          </svg>
                          Subscribing&hellip;
                        </>
                      ) : (
                        <>
                          Subscribe free
                          <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" className="h-4 w-4">
                            <path d="M5 12h13m-5-5 5 5-5 5" />
                          </svg>
                        </>
                      )}
                    </button>

                    <div className="mt-6 flex items-center gap-3 border-t border-neutral-200 pt-5 dark:border-neutral-800">
                      <span aria-hidden="true" className="flex -space-x-2">
                        {["AK", "RM", "JL"].map((n, i) => (
                          <span
                            key={n}
                            className={`flex h-7 w-7 items-center justify-center rounded-full border-2 border-white text-[10px] font-semibold text-white dark:border-neutral-900 ${
                              ["bg-indigo-500", "bg-emerald-500", "bg-rose-500"][i]
                            }`}
                          >
                            {n}
                          </span>
                        ))}
                      </span>
                      <p className="text-xs leading-relaxed text-neutral-500 dark:text-neutral-400">
                        Joined by{" "}
                        <span className="font-semibold text-neutral-800 dark:text-neutral-200">
                          12,400
                        </span>{" "}
                        designers &amp; engineers.
                      </p>
                    </div>
                  </motion.form>
                )}
              </AnimatePresence>

              <p aria-live="polite" className="sr-only">
                {status === "loading"
                  ? "Submitting your subscription."
                  : status === "success"
                    ? `You're on the list — a confirmation is on its way to ${email}.`
                    : ""}
              </p>
            </div>
          </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 →