Web InnoventixFreeCode

Newsletter Footer

Original · free

footer with newsletter

byWeb InnoventixReact + Tailwind
footxnewsletterfooters
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/footx-newsletter.json
footx-newsletter.tsx
"use client";

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

type Status = "idle" | "submitting" | "success" | "error";

const TOPICS = [
  { id: "frontend", label: "Frontend" },
  { id: "backend", label: "Backend" },
  { id: "devops", label: "DevOps & SRE" },
  { id: "ai", label: "AI & ML" },
  { id: "career", label: "Career" },
] as const;

type TopicId = (typeof TOPICS)[number]["id"];

const NAV: { title: string; links: string[] }[] = [
  { title: "Product", links: ["Features", "Changelog", "Pricing", "Integrations", "Roadmap"] },
  { title: "Resources", links: ["Documentation", "Field guides", "API reference", "Community", "Status"] },
  { title: "Company", links: ["About", "Blog", "Careers", "Press kit", "Contact"] },
];

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

const FOCUS =
  "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:focus-visible:ring-indigo-400 dark:focus-visible:ring-offset-slate-950";

function BrandMark() {
  return (
    <span className="inline-flex items-center gap-2.5">
      <span className="relative grid h-9 w-9 place-items-center rounded-xl bg-slate-900 dark:bg-white">
        <svg viewBox="0 0 24 24" className="h-5 w-5" aria-hidden="true">
          <rect x="4" y="12" width="3.2" height="8" rx="1.2" className="fill-white dark:fill-slate-900" />
          <rect x="10.4" y="7" width="3.2" height="13" rx="1.2" className="fill-indigo-400" />
          <rect x="16.8" y="4" width="3.2" height="16" rx="1.2" className="fill-white dark:fill-slate-900" />
        </svg>
      </span>
      <span className="text-lg font-semibold tracking-tight text-slate-900 dark:text-white">Cadence</span>
    </span>
  );
}

const SOCIALS: { name: string; path: string }[] = [
  {
    name: "X",
    path: "M18.244 2.25h3.308l-7.227 8.26 8.502 11.24h-6.653l-5.21-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.71 6.231 5.454-6.231Zm-1.161 17.52h1.833L7.084 4.126H5.117L17.083 19.77Z",
  },
  {
    name: "GitHub",
    path: "M12 2C6.48 2 2 6.58 2 12.26c0 4.5 2.87 8.32 6.84 9.67.5.1.68-.22.68-.48 0-.24-.01-.87-.01-1.7-2.78.62-3.37-1.37-3.37-1.37-.45-1.18-1.11-1.49-1.11-1.49-.91-.63.07-.62.07-.62 1 .07 1.53 1.06 1.53 1.06.89 1.56 2.34 1.11 2.91.85.09-.66.35-1.11.63-1.37-2.22-.26-4.56-1.14-4.56-5.07 0-1.12.39-2.03 1.03-2.75-.1-.26-.45-1.3.1-2.71 0 0 .84-.28 2.75 1.05A9.36 9.36 0 0 1 12 6.84c.85 0 1.71.12 2.51.34 1.91-1.33 2.75-1.05 2.75-1.05.55 1.41.2 2.45.1 2.71.64.72 1.03 1.63 1.03 2.75 0 3.94-2.34 4.81-4.57 5.06.36.32.68.94.68 1.9 0 1.37-.01 2.48-.01 2.82 0 .27.18.59.69.48A10.02 10.02 0 0 0 22 12.26C22 6.58 17.52 2 12 2Z",
  },
  {
    name: "LinkedIn",
    path: "M20.45 20.45h-3.56v-5.57c0-1.33-.02-3.04-1.85-3.04-1.85 0-2.13 1.45-2.13 2.94v5.67H9.35V9h3.42v1.56h.05c.48-.9 1.64-1.85 3.37-1.85 3.6 0 4.27 2.37 4.27 5.45v6.29ZM5.34 7.43a2.07 2.07 0 1 1 0-4.14 2.07 2.07 0 0 1 0 4.14ZM7.12 20.45H3.56V9h3.56v11.45ZM22.22 0H1.77C.79 0 0 .77 0 1.73v20.54C0 23.23.79 24 1.77 24h20.45c.98 0 1.78-.77 1.78-1.73V1.73C24 .77 23.2 0 22.22 0Z",
  },
  {
    name: "RSS",
    path: "M6.18 15.64a2.18 2.18 0 1 1 0 4.36 2.18 2.18 0 0 1 0-4.36ZM4 4.44v2.83c7.03 0 12.73 5.7 12.73 12.73h2.83C19.56 11.35 12.65 4.44 4 4.44Zm0 5.66v2.83a6.9 6.9 0 0 1 6.9 6.9h2.83A9.74 9.74 0 0 0 4 10.1Z",
  },
];

export default function FootxNewsletter() {
  const reduce = useReducedMotion();
  const emailId = useId();
  const errorId = useId();
  const topicsHintId = useId();
  const inputRef = useRef<HTMLInputElement>(null);

  const [email, setEmail] = useState("");
  const [frequency, setFrequency] = useState<"weekly" | "monthly">("weekly");
  const [topics, setTopics] = useState<TopicId[]>(["frontend", "backend"]);
  const [status, setStatus] = useState<Status>("idle");

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

  function handleSubmit(e: FormEvent<HTMLFormElement>) {
    e.preventDefault();
    if (!EMAIL_RE.test(email.trim())) {
      setStatus("error");
      inputRef.current?.focus();
      return;
    }
    setStatus("submitting");
    window.setTimeout(() => setStatus("success"), 900);
  }

  function reset() {
    setStatus("idle");
    setEmail("");
    window.setTimeout(() => inputRef.current?.focus(), 0);
  }

  const rise = reduce
    ? {}
    : {
        initial: { opacity: 0, y: 26 },
        whileInView: { opacity: 1, y: 0 },
        viewport: { once: true, margin: "-90px" },
        transition: { duration: 0.6, ease: [0.22, 1, 0.36, 1] as [number, number, number, number] },
      };

  const selectedLabels = TOPICS.filter((t) => topics.includes(t.id)).map((t) => t.label);

  return (
    <section
      aria-label="Site footer with newsletter"
      className="relative w-full overflow-hidden bg-slate-50 text-slate-900 dark:bg-slate-950 dark:text-slate-100"
    >
      <style>{`
        @keyframes footxnl-float {
          0%, 100% { transform: translate3d(0,0,0) scale(1); }
          50% { transform: translate3d(0,-20px,0) scale(1.07); }
        }
        @keyframes footxnl-pop {
          0% { transform: scale(0.55); opacity: 0; }
          60% { transform: scale(1.1); }
          100% { transform: scale(1); opacity: 1; }
        }
        @keyframes footxnl-sheen {
          0% { transform: translateX(-140%) skewX(-16deg); }
          100% { transform: translateX(260%) skewX(-16deg); }
        }
        .footxnl-orb-a { animation: footxnl-float 17s ease-in-out infinite; }
        .footxnl-orb-b { animation: footxnl-float 23s ease-in-out infinite reverse; }
        .footxnl-pop { animation: footxnl-pop 460ms cubic-bezier(0.22,1,0.36,1) both; }
        .footxnl-sheen { animation: footxnl-sheen 2.6s ease-in-out infinite; }
        @media (prefers-reduced-motion: reduce) {
          .footxnl-orb-a, .footxnl-orb-b, .footxnl-pop, .footxnl-sheen { animation: none !important; }
        }
      `}</style>

      {/* atmosphere */}
      <div aria-hidden="true" className="pointer-events-none absolute inset-0">
        <div className="footxnl-orb-a absolute -top-28 -right-16 h-80 w-80 rounded-full bg-indigo-500/15 blur-3xl dark:bg-indigo-500/25" />
        <div className="footxnl-orb-b absolute -bottom-32 -left-20 h-96 w-96 rounded-full bg-violet-500/10 blur-3xl dark:bg-violet-600/20" />
        <div className="absolute inset-x-0 top-0 h-px bg-gradient-to-r from-transparent via-slate-300 to-transparent dark:via-slate-700" />
      </div>

      <div className="relative mx-auto max-w-6xl px-6 py-20 sm:px-8 sm:py-24">
        {/* ── Newsletter band ── */}
        <div className="grid items-start gap-12 lg:grid-cols-2 lg:gap-16">
          <motion.div {...rise}>
            <span className="inline-flex items-center gap-2 rounded-full border border-slate-300/70 bg-white/60 px-3 py-1 text-xs font-medium uppercase tracking-widest text-slate-600 backdrop-blur dark:border-slate-700 dark:bg-white/5 dark:text-slate-400">
              <span className="h-1.5 w-1.5 rounded-full bg-emerald-500" />
              The Cadence Dispatch
            </span>
            <h2 className="mt-6 text-4xl font-semibold leading-[1.05] tracking-tight text-slate-900 sm:text-5xl dark:text-white">
              Ship better software,
              <span className="block bg-gradient-to-r from-indigo-600 to-violet-500 bg-clip-text text-transparent dark:from-indigo-400 dark:to-violet-300">
                one issue at a time.
              </span>
            </h2>
            <p className="mt-5 max-w-md text-base leading-relaxed text-slate-600 dark:text-slate-400">
              A hand-edited digest of engineering essays, tooling teardowns, and honest postmortems. No
              press releases, no growth-hacks — just the writing we wish landed in our own inboxes.
            </p>

            <div className="mt-8 flex items-center gap-4">
              <div className="flex -space-x-2">
                {[
                  { i: "AR", c: "bg-indigo-500" },
                  { i: "JK", c: "bg-emerald-500" },
                  { i: "MØ", c: "bg-rose-500" },
                  { i: "PL", c: "bg-amber-500" },
                ].map((a) => (
                  <span
                    key={a.i}
                    className={`grid h-8 w-8 place-items-center rounded-full text-[10px] font-semibold text-white ring-2 ring-slate-50 dark:ring-slate-950 ${a.c}`}
                  >
                    {a.i}
                  </span>
                ))}
              </div>
              <p className="text-sm text-slate-500 dark:text-slate-400">
                Read by <span className="font-semibold text-slate-800 dark:text-slate-200">74,200+</span> engineers
              </p>
            </div>
          </motion.div>

          {/* Form card */}
          <motion.div
            {...rise}
            className="rounded-3xl border border-slate-200 bg-white/80 p-6 shadow-xl shadow-slate-900/5 backdrop-blur-sm sm:p-8 dark:border-slate-800 dark:bg-slate-900/60 dark:shadow-black/40"
          >
            {status === "success" ? (
              <div role="status" aria-live="polite" className="py-4 text-center">
                <span className="footxnl-pop mx-auto grid h-14 w-14 place-items-center rounded-full bg-emerald-500/15 text-emerald-600 dark:text-emerald-400">
                  <svg viewBox="0 0 24 24" fill="none" className="h-7 w-7" aria-hidden="true">
                    <path d="M20 6 9 17l-5-5" stroke="currentColor" strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round" />
                  </svg>
                </span>
                <h3 className="mt-5 text-xl font-semibold text-slate-900 dark:text-white">You&rsquo;re on the list.</h3>
                <p className="mx-auto mt-2 max-w-xs text-sm text-slate-600 dark:text-slate-400">
                  We sent a confirmation to{" "}
                  <span className="font-medium text-slate-900 dark:text-slate-200">{email.trim()}</span>. Your{" "}
                  <span className="font-medium text-slate-900 dark:text-slate-200">{frequency}</span> issue covers{" "}
                  {selectedLabels.length > 0 ? selectedLabels.join(", ") : "everything we publish"}.
                </p>
                <button
                  type="button"
                  onClick={reset}
                  className={`mt-6 inline-flex items-center gap-1.5 rounded-lg px-3 py-2 text-sm font-medium text-indigo-600 transition hover:bg-indigo-50 dark:text-indigo-400 dark:hover:bg-indigo-500/10 ${FOCUS}`}
                >
                  Use a different email
                </button>
              </div>
            ) : (
              <form onSubmit={handleSubmit} noValidate>
                <label htmlFor={emailId} className="block text-sm font-medium text-slate-800 dark:text-slate-200">
                  Work email
                </label>
                <input
                  ref={inputRef}
                  id={emailId}
                  type="email"
                  inputMode="email"
                  autoComplete="email"
                  placeholder="you@company.dev"
                  value={email}
                  onChange={(e) => {
                    setEmail(e.target.value);
                    if (status === "error") setStatus("idle");
                  }}
                  aria-invalid={status === "error"}
                  aria-describedby={status === "error" ? errorId : undefined}
                  className={`mt-2 w-full rounded-xl border bg-white px-4 py-3 text-slate-900 placeholder:text-slate-400 transition dark:bg-slate-950/60 dark:text-white dark:placeholder:text-slate-500 ${FOCUS} ${
                    status === "error"
                      ? "border-rose-400 dark:border-rose-500"
                      : "border-slate-300 dark:border-slate-700"
                  }`}
                />
                {status === "error" && (
                  <p id={errorId} 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" className="h-4 w-4 shrink-0" aria-hidden="true">
                      <circle cx="12" cy="12" r="9" stroke="currentColor" strokeWidth="2" />
                      <path d="M12 7v6M12 16.5v.5" stroke="currentColor" strokeWidth="2" strokeLinecap="round" />
                    </svg>
                    Please enter a valid email address.
                  </p>
                )}

                {/* Frequency */}
                <fieldset className="mt-6">
                  <legend className="text-sm font-medium text-slate-800 dark:text-slate-200">Cadence</legend>
                  <div className="mt-2 grid grid-cols-2 gap-2 rounded-xl border border-slate-200 bg-slate-100/70 p-1 dark:border-slate-800 dark:bg-slate-950/50">
                    {(["weekly", "monthly"] as const).map((f) => (
                      <label
                        key={f}
                        className={`relative cursor-pointer rounded-lg px-3 py-2 text-center text-sm font-medium capitalize transition ${
                          frequency === f
                            ? "bg-white text-slate-900 shadow-sm dark:bg-slate-800 dark:text-white"
                            : "text-slate-500 hover:text-slate-800 dark:text-slate-400 dark:hover:text-slate-200"
                        }`}
                      >
                        <input
                          type="radio"
                          name="footxnl-frequency"
                          value={f}
                          checked={frequency === f}
                          onChange={() => setFrequency(f)}
                          className="peer sr-only"
                        />
                        <span className="pointer-events-none absolute inset-0 rounded-lg ring-2 ring-transparent peer-focus-visible:ring-indigo-500 dark:peer-focus-visible:ring-indigo-400" />
                        {f === "weekly" ? "Weekly" : "Monthly"}
                      </label>
                    ))}
                  </div>
                </fieldset>

                {/* Topics */}
                <div className="mt-6">
                  <p id={topicsHintId} className="text-sm font-medium text-slate-800 dark:text-slate-200">
                    Tune your feed
                  </p>
                  <div className="mt-2 flex flex-wrap gap-2" role="group" aria-labelledby={topicsHintId}>
                    {TOPICS.map((t) => {
                      const on = topics.includes(t.id);
                      return (
                        <button
                          key={t.id}
                          type="button"
                          aria-pressed={on}
                          onClick={() => toggleTopic(t.id)}
                          className={`inline-flex items-center gap-1.5 rounded-full border px-3 py-1.5 text-sm font-medium transition ${FOCUS} ${
                            on
                              ? "border-indigo-500 bg-indigo-500/10 text-indigo-700 dark:border-indigo-400 dark:text-indigo-300"
                              : "border-slate-300 bg-transparent text-slate-600 hover:border-slate-400 dark:border-slate-700 dark:text-slate-400 dark:hover:border-slate-600"
                          }`}
                        >
                          <span
                            aria-hidden="true"
                            className={`grid h-4 w-4 place-items-center rounded-full border transition ${
                              on ? "border-indigo-500 bg-indigo-500 dark:border-indigo-400 dark:bg-indigo-400" : "border-slate-400 dark:border-slate-600"
                            }`}
                          >
                            {on && (
                              <svg viewBox="0 0 24 24" fill="none" className="h-3 w-3 text-white dark:text-slate-950">
                                <path d="M20 6 9 17l-5-5" stroke="currentColor" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round" />
                              </svg>
                            )}
                          </span>
                          {t.label}
                        </button>
                      );
                    })}
                  </div>
                </div>

                <button
                  type="submit"
                  disabled={status === "submitting"}
                  className={`group relative mt-7 flex w-full items-center justify-center gap-2 overflow-hidden rounded-xl bg-slate-900 px-5 py-3.5 text-sm font-semibold text-white transition hover:bg-slate-800 disabled:cursor-not-allowed disabled:opacity-70 dark:bg-white dark:text-slate-900 dark:hover:bg-slate-100 ${FOCUS}`}
                >
                  <span className="footxnl-sheen pointer-events-none absolute inset-y-0 -left-1/2 w-1/3 bg-white/25 dark:bg-slate-900/10" aria-hidden="true" />
                  {status === "submitting" ? (
                    <>
                      <svg viewBox="0 0 24 24" fill="none" className="h-4 w-4 animate-spin" aria-hidden="true">
                        <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&hellip;
                    </>
                  ) : (
                    <>
                      Subscribe for free
                      <svg viewBox="0 0 24 24" fill="none" className="h-4 w-4 transition-transform group-hover:translate-x-0.5" aria-hidden="true">
                        <path d="M5 12h14M13 6l6 6-6 6" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
                      </svg>
                    </>
                  )}
                </button>
                <p className="mt-3 text-center text-xs text-slate-500 dark:text-slate-500">
                  One email per {frequency === "weekly" ? "week" : "month"}. Unsubscribe in a click. We never sell your data.
                </p>
              </form>
            )}
          </motion.div>
        </div>

        {/* ── Nav ── */}
        <div className="mt-20 grid gap-12 border-t border-slate-200 pt-14 sm:grid-cols-2 lg:grid-cols-5 dark:border-slate-800">
          <div className="lg:col-span-2">
            <BrandMark />
            <p className="mt-4 max-w-xs text-sm leading-relaxed text-slate-600 dark:text-slate-400">
              Tools and writing for teams who care about how software gets built. Independent since 2019.
            </p>
            <div className="mt-6 flex gap-2">
              {SOCIALS.map((s) => (
                <a
                  key={s.name}
                  href="#"
                  target="_blank"
                  rel="noopener"
                  aria-label={`Cadence on ${s.name}`}
                  className={`grid h-10 w-10 place-items-center rounded-xl border border-slate-200 text-slate-500 transition hover:border-slate-300 hover:text-slate-900 dark:border-slate-800 dark:text-slate-400 dark:hover:border-slate-600 dark:hover:text-white ${FOCUS}`}
                >
                  <svg viewBox="0 0 24 24" fill="currentColor" className="h-5 w-5" aria-hidden="true">
                    <path d={s.path} />
                  </svg>
                </a>
              ))}
            </div>
          </div>

          {NAV.map((col) => (
            <nav key={col.title} aria-label={col.title}>
              <h3 className="text-sm font-semibold text-slate-900 dark:text-white">{col.title}</h3>
              <ul className="mt-4 space-y-3">
                {col.links.map((link) => (
                  <li key={link}>
                    <a
                      href="#"
                      className={`rounded text-sm text-slate-600 transition hover:text-slate-900 dark:text-slate-400 dark:hover:text-white ${FOCUS}`}
                    >
                      {link}
                    </a>
                  </li>
                ))}
              </ul>
            </nav>
          ))}
        </div>

        {/* ── Bottom bar ── */}
        <div className="mt-14 flex flex-col gap-4 border-t border-slate-200 pt-8 text-sm text-slate-500 sm:flex-row sm:items-center sm:justify-between dark:border-slate-800 dark:text-slate-400">
          <p>&copy; 2026 Cadence Media, Inc. All rights reserved.</p>
          <ul className="flex flex-wrap items-center gap-x-6 gap-y-2">
            {["Privacy", "Terms", "Security", "Cookie settings"].map((l) => (
              <li key={l}>
                <a href="#" className={`rounded transition hover:text-slate-900 dark:hover:text-white ${FOCUS}`}>
                  {l}
                </a>
              </li>
            ))}
          </ul>
        </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 →