Web InnoventixFreeCode

Dark Newsletter

Original · free

dark newsletter section

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

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

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

const CADENCES: { id: Cadence; label: string; hint: string }[] = [
  { id: "weekly", label: "Weekly", hint: "every Thursday" },
  { id: "biweekly", label: "Biweekly", hint: "every other Thursday" },
  { id: "monthly", label: "Monthly", hint: "on the first of the month" },
];

const TOPICS = [
  "Motion",
  "Design systems",
  "Performance",
  "Accessibility",
  "CSS & layout",
  "Dev tooling",
];

const ISSUES = [
  { no: "184", title: "Making view transitions feel native" },
  { no: "183", title: "The real cost of a single re-render" },
  { no: "182", title: "Focus management users can actually feel" },
];

const AVATARS: { initials: string; grad: string }[] = [
  { initials: "AK", grad: "from-indigo-400 to-violet-500" },
  { initials: "MR", grad: "from-sky-400 to-indigo-500" },
  { initials: "TL", grad: "from-violet-400 to-rose-400" },
  { initials: "JD", grad: "from-emerald-400 to-sky-500" },
];

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

const styles = `
@keyframes newsxDark-rise {
  from { opacity: 0; transform: translateY(16px); }
  to { opacity: 1; transform: translateY(0); }
}
@keyframes newsxDark-auroraA {
  0%, 100% { transform: translate3d(0, 0, 0) scale(1); }
  50% { transform: translate3d(7%, -5%, 0) scale(1.18); }
}
@keyframes newsxDark-auroraB {
  0%, 100% { transform: translate3d(0, 0, 0) scale(1.12); }
  50% { transform: translate3d(-6%, 6%, 0) scale(1); }
}
@keyframes newsxDark-spin { to { transform: rotate(360deg); } }
@keyframes newsxDark-draw { to { stroke-dashoffset: 0; } }
.newsxDark-auroraA { animation: newsxDark-auroraA 20s ease-in-out infinite; }
.newsxDark-auroraB { animation: newsxDark-auroraB 26s ease-in-out infinite; }
.newsxDark-spin { animation: newsxDark-spin 0.7s linear infinite; }
.newsxDark-draw { stroke-dasharray: 26; stroke-dashoffset: 26; animation: newsxDark-draw 0.5s ease 0.05s forwards; }
@media (prefers-reduced-motion: reduce) {
  .newsxDark-auroraA,
  .newsxDark-auroraB,
  .newsxDark-spin,
  .newsxDark-draw {
    animation: none !important;
    stroke-dashoffset: 0 !important;
  }
}
`;

export default function NewsxDark() {
  const reduce = useReducedMotion();
  const [email, setEmail] = useState("");
  const [emailError, setEmailError] = useState<string | null>(null);
  const [cadence, setCadence] = useState<Cadence>("weekly");
  const [topics, setTopics] = useState<string[]>(["Motion", "Performance"]);
  const [status, setStatus] = useState<Status>("idle");
  const inputRef = useRef<HTMLInputElement>(null);
  const timeoutRef = useRef<number | null>(null);

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

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

  function handleSubmit(event: FormEvent<HTMLFormElement>) {
    event.preventDefault();
    if (status === "submitting") return;
    if (!EMAIL_RE.test(email.trim())) {
      setEmailError("Enter a valid email so we can reach you.");
      inputRef.current?.focus();
      return;
    }
    setEmailError(null);
    setStatus("submitting");
    timeoutRef.current = window.setTimeout(() => setStatus("success"), 1200);
  }

  function reset() {
    setStatus("idle");
    setEmail("");
    setCadence("weekly");
    setTopics(["Motion", "Performance"]);
    requestAnimationFrame(() => inputRef.current?.focus());
  }

  const activeCadence = CADENCES.find((c) => c.id === cadence) ?? CADENCES[0];

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

  const swapMotion = reduce
    ? {}
    : {
        initial: { opacity: 0, y: 10 },
        animate: { opacity: 1, y: 0 },
        exit: { opacity: 0, y: -10 },
        transition: { duration: 0.28, ease: [0.22, 1, 0.36, 1] as const },
      };

  const errorId = "newsxDark-email-error";

  return (
    <section className="relative w-full overflow-hidden bg-slate-50 text-slate-900 dark:bg-slate-950 dark:text-slate-100">
      <style>{styles}</style>

      {/* Atmosphere */}
      <div aria-hidden className="pointer-events-none absolute inset-0 overflow-hidden">
        <div className="newsxDark-auroraA absolute -top-40 -left-24 h-[28rem] w-[28rem] rounded-full bg-indigo-300/40 blur-3xl dark:bg-indigo-600/25" />
        <div className="newsxDark-auroraB absolute -bottom-48 -right-24 h-[32rem] w-[32rem] rounded-full bg-violet-300/40 blur-3xl dark:bg-violet-600/20" />
        <div
          className="absolute inset-0 opacity-[0.5] dark:opacity-[0.35]"
          style={{
            backgroundImage:
              "radial-gradient(currentColor 1px, transparent 1px)",
            backgroundSize: "22px 22px",
            color: "rgb(100 116 139 / 0.14)",
            maskImage:
              "radial-gradient(ellipse 80% 60% at 50% 40%, black, transparent)",
            WebkitMaskImage:
              "radial-gradient(ellipse 80% 60% at 50% 40%, black, transparent)",
          }}
        />
      </div>

      <div className="relative mx-auto max-w-6xl px-6 py-20 sm:px-8 sm:py-28">
        <div className="grid items-start gap-12 lg:grid-cols-[1.05fr_1fr] lg:gap-16">
          {/* Left: pitch */}
          <motion.div {...riseMotion} 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-medium tracking-wide text-slate-600 backdrop-blur dark:border-white/10 dark:bg-white/5 dark:text-slate-300">
              <span className="relative flex h-2 w-2">
                <span className="absolute inline-flex h-full w-full rounded-full bg-emerald-400/70 motion-safe:animate-ping" />
                <span className="relative inline-flex h-2 w-2 rounded-full bg-emerald-500" />
              </span>
              The Frontier 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 interfaces,{" "}
              <span className="bg-gradient-to-r from-indigo-500 to-violet-500 bg-clip-text text-transparent dark:from-indigo-400 dark:to-violet-400">
                every Thursday.
              </span>
            </h2>

            <p className="mt-5 text-lg leading-relaxed text-slate-600 dark:text-slate-300">
              A no-filler dispatch on frontend engineering, motion, and interface
              design. One technique you can ship this week, worked end to end, plus
              the tools and writing worth your attention.
            </p>

            {/* Proof */}
            <div className="mt-8 flex flex-wrap items-center gap-x-6 gap-y-4">
              <div className="flex items-center">
                <ul className="flex -space-x-2">
                  {AVATARS.map((a) => (
                    <li
                      key={a.initials}
                      className={`grid h-9 w-9 place-items-center rounded-full bg-gradient-to-br ${a.grad} text-[0.7rem] font-semibold text-white ring-2 ring-slate-50 dark:ring-slate-950`}
                    >
                      {a.initials}
                    </li>
                  ))}
                </ul>
                <p className="ml-3 text-sm text-slate-600 dark:text-slate-300">
                  <span className="font-semibold text-slate-900 dark:text-white">
                    24,847
                  </span>{" "}
                  engineers &amp; designers
                </p>
              </div>
              <div className="flex items-center gap-1.5">
                <div className="flex" aria-hidden>
                  {Array.from({ length: 5 }).map((_, i) => (
                    <svg
                      key={i}
                      viewBox="0 0 20 20"
                      className="h-4 w-4 fill-amber-400"
                    >
                      <path d="M10 1.5l2.6 5.3 5.9.9-4.3 4.1 1 5.8L10 15l-5.2 2.7 1-5.8L1.5 7.7l5.9-.9L10 1.5z" />
                    </svg>
                  ))}
                </div>
                <p className="text-sm text-slate-600 dark:text-slate-300">
                  <span className="font-semibold text-slate-900 dark:text-white">
                    4.9
                  </span>{" "}
                  <span className="text-slate-500 dark:text-slate-400">
                    from 800+ readers
                  </span>
                </p>
              </div>
            </div>

            {/* Recent issues */}
            <div className="mt-10 rounded-2xl border border-slate-200 bg-white/60 p-5 backdrop-blur dark:border-white/10 dark:bg-white/[0.03]">
              <p className="text-xs font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-400">
                Recent issues
              </p>
              <ul className="mt-3 divide-y divide-slate-200/70 dark:divide-white/5">
                {ISSUES.map((issue) => (
                  <li
                    key={issue.no}
                    className="flex items-center gap-3 py-2.5 text-sm"
                  >
                    <span className="shrink-0 rounded-md bg-slate-900/5 px-2 py-0.5 font-mono text-xs text-slate-500 dark:bg-white/10 dark:text-slate-400">
                      #{issue.no}
                    </span>
                    <span className="text-slate-700 dark:text-slate-200">
                      {issue.title}
                    </span>
                  </li>
                ))}
              </ul>
            </div>
          </motion.div>

          {/* Right: form card */}
          <motion.div
            {...riseMotion}
            transition={
              reduce
                ? undefined
                : { duration: 0.6, delay: 0.08, ease: [0.22, 1, 0.36, 1] as const }
            }
            className="relative rounded-3xl border border-slate-200 bg-white/80 p-6 shadow-xl shadow-slate-900/5 backdrop-blur-xl sm:p-8 dark:border-white/10 dark:bg-white/[0.04] dark:shadow-black/40"
          >
            <div
              aria-hidden
              className="pointer-events-none absolute inset-x-0 -top-px h-px bg-gradient-to-r from-transparent via-indigo-400/60 to-transparent"
            />

            <AnimatePresence mode="wait" initial={false}>
              {status === "success" ? (
                <motion.div
                  key="success"
                  {...swapMotion}
                  role="status"
                  aria-live="polite"
                  className="flex flex-col items-start"
                >
                  <span className="grid h-12 w-12 place-items-center rounded-2xl bg-emerald-500/15 text-emerald-600 dark:text-emerald-400">
                    <svg
                      viewBox="0 0 24 24"
                      className="h-6 w-6"
                      fill="none"
                      stroke="currentColor"
                      strokeWidth={2.5}
                      strokeLinecap="round"
                      strokeLinejoin="round"
                    >
                      <path className="newsxDark-draw" d="M20 6L9 17l-5-5" />
                    </svg>
                  </span>
                  <h3 className="mt-5 text-2xl font-semibold tracking-tight text-slate-900 dark:text-white">
                    You&apos;re in. Welcome aboard.
                  </h3>
                  <p className="mt-2 text-slate-600 dark:text-slate-300">
                    We sent a confirmation to{" "}
                    <span className="font-medium text-slate-900 dark:text-white">
                      {email}
                    </span>
                    . Your first {activeCadence.label.toLowerCase()} issue lands{" "}
                    {activeCadence.hint}
                    {topics.length > 0
                      ? `, tuned to ${topics.length} topic${
                          topics.length > 1 ? "s" : ""
                        }.`
                      : "."}
                  </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-colors hover:bg-indigo-50 focus:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 dark:text-indigo-300 dark:hover:bg-indigo-500/10"
                  >
                    Subscribe another email
                    <svg
                      viewBox="0 0 20 20"
                      className="h-4 w-4"
                      fill="none"
                      stroke="currentColor"
                      strokeWidth={2}
                      strokeLinecap="round"
                      strokeLinejoin="round"
                    >
                      <path d="M4 10h12M11 5l5 5-5 5" />
                    </svg>
                  </button>
                </motion.div>
              ) : (
                <motion.form
                  key="form"
                  {...swapMotion}
                  onSubmit={handleSubmit}
                  noValidate
                  className="flex flex-col gap-6"
                >
                  <div>
                    <h3 className="text-xl font-semibold tracking-tight text-slate-900 dark:text-white">
                      Start reading
                    </h3>
                    <p className="mt-1 text-sm text-slate-500 dark:text-slate-400">
                      Free, forever. Unsubscribe in one click.
                    </p>
                  </div>

                  {/* Email */}
                  <div>
                    <label
                      htmlFor="newsxDark-email"
                      className="block text-sm font-medium text-slate-700 dark:text-slate-200"
                    >
                      Email address
                    </label>
                    <input
                      ref={inputRef}
                      id="newsxDark-email"
                      type="email"
                      name="email"
                      autoComplete="email"
                      inputMode="email"
                      placeholder="you@studio.dev"
                      value={email}
                      onChange={(e) => {
                        setEmail(e.target.value);
                        if (emailError) setEmailError(null);
                      }}
                      aria-invalid={emailError ? true : undefined}
                      aria-describedby={emailError ? errorId : undefined}
                      className={`mt-2 w-full rounded-xl border bg-white px-4 py-3 text-slate-900 placeholder:text-slate-400 focus:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500/60 dark:bg-slate-900/60 dark:text-white dark:placeholder:text-slate-500 ${
                        emailError
                          ? "border-rose-400 focus:border-rose-500 dark:border-rose-500/70"
                          : "border-slate-300 focus:border-indigo-500 dark:border-white/10 dark:focus:border-indigo-400"
                      }`}
                    />
                    {emailError ? (
                      <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 20 20"
                          className="h-4 w-4 shrink-0"
                          fill="currentColor"
                          aria-hidden
                        >
                          <path
                            fillRule="evenodd"
                            d="M10 18a8 8 0 100-16 8 8 0 000 16zm-.75-11.5a.75.75 0 011.5 0v4a.75.75 0 01-1.5 0v-4zM10 14a1 1 0 100-2 1 1 0 000 2z"
                            clipRule="evenodd"
                          />
                        </svg>
                        {emailError}
                      </p>
                    ) : null}
                  </div>

                  {/* Cadence */}
                  <fieldset>
                    <legend className="text-sm font-medium text-slate-700 dark:text-slate-200">
                      How often?
                    </legend>
                    <div className="mt-2 grid grid-cols-3 gap-2">
                      {CADENCES.map((c) => (
                        <label
                          key={c.id}
                          className="relative block cursor-pointer"
                        >
                          <input
                            type="radio"
                            name="newsxDark-cadence"
                            value={c.id}
                            checked={cadence === c.id}
                            onChange={() => setCadence(c.id)}
                            className="peer sr-only"
                          />
                          <span className="flex h-full flex-col items-center justify-center rounded-xl border border-slate-300 bg-white px-2 py-2.5 text-center text-sm font-medium text-slate-600 transition-colors peer-checked:border-indigo-500 peer-checked:bg-indigo-50 peer-checked:text-indigo-700 peer-focus-visible:ring-2 peer-focus-visible:ring-indigo-500/60 dark:border-white/10 dark:bg-slate-900/50 dark:text-slate-300 dark:peer-checked:border-indigo-400 dark:peer-checked:bg-indigo-500/15 dark:peer-checked:text-indigo-200">
                            {c.label}
                          </span>
                        </label>
                      ))}
                    </div>
                  </fieldset>

                  {/* Topics */}
                  <fieldset>
                    <legend className="text-sm font-medium text-slate-700 dark:text-slate-200">
                      Tune your feed{" "}
                      <span className="font-normal text-slate-400 dark:text-slate-500">
                        (optional)
                      </span>
                    </legend>
                    <div className="mt-2 flex flex-wrap gap-2">
                      {TOPICS.map((topic) => {
                        const active = topics.includes(topic);
                        return (
                          <button
                            key={topic}
                            type="button"
                            aria-pressed={active}
                            onClick={() => toggleTopic(topic)}
                            className={`inline-flex items-center gap-1.5 rounded-full border px-3 py-1.5 text-sm font-medium transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500/60 ${
                              active
                                ? "border-indigo-500 bg-indigo-500 text-white dark:border-indigo-400 dark:bg-indigo-500"
                                : "border-slate-300 bg-white text-slate-600 hover:border-slate-400 dark:border-white/10 dark:bg-slate-900/50 dark:text-slate-300 dark:hover:border-white/20"
                            }`}
                          >
                            <svg
                              viewBox="0 0 20 20"
                              className={`h-3.5 w-3.5 transition-transform ${
                                active ? "scale-100" : "scale-0"
                              }`}
                              fill="none"
                              stroke="currentColor"
                              strokeWidth={2.5}
                              strokeLinecap="round"
                              strokeLinejoin="round"
                              aria-hidden
                            >
                              <path d="M4 10l4 4 8-9" />
                            </svg>
                            {topic}
                          </button>
                        );
                      })}
                    </div>
                  </fieldset>

                  {/* Submit */}
                  <div>
                    <button
                      type="submit"
                      disabled={status === "submitting"}
                      className="group inline-flex w-full items-center justify-center gap-2 rounded-xl bg-slate-900 px-5 py-3.5 text-sm font-semibold text-white shadow-lg shadow-slate-900/20 transition-colors hover:bg-slate-800 focus: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-70 dark:bg-white dark:text-slate-900 dark:shadow-black/30 dark:hover:bg-slate-100 dark:focus-visible:ring-offset-slate-950"
                    >
                      {status === "submitting" ? (
                        <>
                          <svg
                            viewBox="0 0 24 24"
                            className="newsxDark-spin h-4 w-4"
                            fill="none"
                            aria-hidden
                          >
                            <circle
                              cx="12"
                              cy="12"
                              r="9"
                              stroke="currentColor"
                              strokeWidth="3"
                              className="opacity-25"
                            />
                            <path
                              d="M21 12a9 9 0 00-9-9"
                              stroke="currentColor"
                              strokeWidth="3"
                              strokeLinecap="round"
                            />
                          </svg>
                          Subscribing…
                        </>
                      ) : (
                        <>
                          Subscribe free
                          <svg
                            viewBox="0 0 20 20"
                            className="h-4 w-4 transition-transform group-hover:translate-x-0.5"
                            fill="none"
                            stroke="currentColor"
                            strokeWidth={2}
                            strokeLinecap="round"
                            strokeLinejoin="round"
                            aria-hidden
                          >
                            <path d="M4 10h12M11 5l5 5-5 5" />
                          </svg>
                        </>
                      )}
                    </button>
                    <p className="mt-3 flex items-center justify-center gap-1.5 text-xs text-slate-500 dark:text-slate-400">
                      <svg
                        viewBox="0 0 20 20"
                        className="h-3.5 w-3.5"
                        fill="currentColor"
                        aria-hidden
                      >
                        <path
                          fillRule="evenodd"
                          d="M10 1.5l6 2.4v4.6c0 3.9-2.6 7.4-6 8.5-3.4-1.1-6-4.6-6-8.5V3.9l6-2.4zm2.9 6.4a.75.75 0 00-1.15-.96l-2.7 3.24-1.3-1.3a.75.75 0 10-1.06 1.06l1.88 1.88a.75.75 0 001.11-.06l3.22-3.86z"
                          clipRule="evenodd"
                        />
                      </svg>
                      No spam, ever. We never share your email.
                    </p>
                  </div>
                </motion.form>
              )}
            </AnimatePresence>
          </motion.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 →