# Banner Newsletter Inline — React Banner Component

> An in-article signup banner: on submit the paper plane flies off, the field collapses and a confirmation line draws itself in.

- Category: [Banners](https://pixelpapa.com/components/banners.md)
- Tags: banner, newsletter, email, inline, form
- Dependencies: lucide-react, motion (plus React and Tailwind CSS v4)
- Last updated: 2026-11-13
- Live preview: https://pixelpapa.com/components/banners/banner-newsletter-inline

## Install

```bash
npx shadcn@latest add "https://pixelpapa.com/r/banner-newsletter-inline.json"
```

The file is written to `components/pixelpapa/banner-newsletter-inline.tsx`. To install manually, run `npm install lucide-react motion` and copy the source below into your project. It imports `cn` from `@/lib/utils`, which every shadcn project has.

## Source

```tsx
"use client";

import { Check, Send } from "lucide-react";
import { AnimatePresence, motion } from "motion/react";
import { useId, useState, type FormEvent } from "react";
import { cn } from "@/lib/utils";

type BannerNewsletterInlineProps = {
  title?: string;
  body?: string;
  onSubscribe?: (email: string) => void;
  className?: string;
};

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

export function BannerNewsletterInline({
  title = "Enjoying this? Get the next one by email.",
  body = "One essay every other Sunday. 18,400 readers, no sponsors.",
  onSubscribe,
  className,
}: BannerNewsletterInlineProps) {
  const id = useId();
  const [email, setEmail] = useState("");
  const [done, setDone] = useState(false);
  const [invalid, setInvalid] = useState(false);

  function submit(e: FormEvent) {
    e.preventDefault();
    if (!/^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(email)) return setInvalid(true);
    setDone(true);
    onSubscribe?.(email);
  }

  return (
    <aside
      aria-label="Newsletter"
      className={cn(
        "relative flex flex-col gap-5 overflow-hidden rounded-2xl border border-border bg-card p-5 sm:flex-row sm:items-center sm:p-6",
        className,
      )}
    >
      <span aria-hidden className="absolute inset-y-0 left-0 w-1 bg-primary" />
      <div className="relative flex size-12 shrink-0 items-center justify-center rounded-full bg-primary/10 text-primary">
        <AnimatePresence mode="popLayout" initial={false}>
          {done ? (
            <motion.span
              key="check"
              initial={{ scale: 0, rotate: -45 }}
              animate={{ scale: 1, rotate: 0 }}
              transition={{ type: "spring", stiffness: 420, damping: 18, delay: 0.25 }}
            >
              <Check className="size-5" strokeWidth={3} />
            </motion.span>
          ) : (
            <motion.span key="plane" exit={{ x: 60, y: -60, opacity: 0, scale: 0.6 }} transition={{ duration: 0.45, ease: "easeIn" }}>
              <Send className="size-5" />
            </motion.span>
          )}
        </AnimatePresence>
      </div>

      <div className="min-w-0 flex-1">
        <p className="font-medium text-card-foreground">{done ? "You’re on the list." : title}</p>
        <p className="mt-0.5 text-sm text-muted-foreground" role={done ? "status" : undefined}>
          {done ? `The next issue goes to ${email} on Sunday.` : body}
        </p>
      </div>

      <AnimatePresence initial={false}>
        {!done && (
          <motion.form
            onSubmit={submit}
            noValidate
            exit={{ width: 0, opacity: 0, marginLeft: -20 }}
            transition={{ duration: 0.5, ease }}
            className="shrink-0 overflow-hidden sm:w-[330px]"
          >
            <div
              className={cn(
                "flex h-11 items-center rounded-full border bg-background p-1 pl-4 transition-[border-color,box-shadow] focus-within:ring-4",
                invalid ? "border-destructive focus-within:ring-destructive/15" : "border-input focus-within:border-ring focus-within:ring-ring/15",
              )}
            >
              <label htmlFor={id} className="sr-only">
                Email address
              </label>
              <input
                id={id}
                type="email"
                value={email}
                aria-invalid={invalid}
                onChange={(e) => {
                  setEmail(e.target.value);
                  setInvalid(false);
                }}
                placeholder={invalid ? "Enter a valid email" : "you@example.com"}
                className={cn(
                  "h-full min-w-0 flex-1 bg-transparent text-sm text-foreground outline-none",
                  invalid ? "placeholder:text-destructive" : "placeholder:text-muted-foreground",
                )}
              />
              <button type="submit" className="h-full shrink-0 rounded-full bg-primary px-4 text-sm font-medium whitespace-nowrap text-primary-foreground transition-transform active:scale-95">
                Subscribe
              </button>
            </div>
          </motion.form>
        )}
      </AnimatePresence>
    </aside>
  );
}

export default function BannerNewsletterInlineDemo() {
  return (
    <div className="w-full bg-background px-6 py-12">
      <article className="mx-auto max-w-2xl">
        <p className="text-xs tracking-[0.18em] text-primary uppercase">Notes on making · Nº 61</p>
        <h1 className="mt-3 font-serif text-4xl leading-tight text-foreground">The case for finishing small things</h1>
        <p className="mt-5 text-lg leading-relaxed text-foreground/80">
          Most of what I learned about craft came from projects that fit inside a weekend. A small thing can be finished,
          and finishing is where the lessons hide: in the last tenth, when the idea meets its edges.
        </p>
        <BannerNewsletterInline className="my-8" />
        <p className="text-lg leading-relaxed text-foreground/80">
          A large project lets you defer every hard decision to a future version. A small one has no future version. You
          choose the typeface today, you ship the rough corner or you sand it, and either way you find out.
        </p>
      </article>
    </div>
  );
}
```

## AI prompt

```text
Add the "Banner Newsletter Inline" component from PixelPapa UI to this project.

What it is: An in-article signup banner: on submit the paper plane flies off, the field collapses and a confirmation line draws itself in.
Category: Banners. Depends on: lucide-react, motion.

1. Install it with the shadcn CLI:
   npx shadcn@latest add "https://pixelpapa.com/r/banner-newsletter-inline.json"
   (If the CLI is unavailable, fetch that URL, write files[0].content to components/pixelpapa/banner-newsletter-inline.tsx and install the listed dependencies.)
2. Import it from "@/components/pixelpapa/banner-newsletter-inline" and place it where I describe below.
3. Keep its animation timings and accessibility attributes. Replace the demo copy and data with content that fits my product, and use my existing theme tokens (bg-background, text-foreground, bg-primary, border-border) rather than hard-coded colours.

Where to use it: <describe the page and position here>
```

## More banners

- [Banner Announcement Marquee](https://pixelpapa.com/components/banners/banner-announcement-marquee.md): A top strip that scrolls a seamless loop of announcements between faded edges, pauses on hover or focus and collapses when dismissed.
- [Banner Cookie Consent](https://pixelpapa.com/components/banners/banner-cookie-consent.md) (Pro): A corner consent card whose preferences panel unfolds with spring height, with real switches per category and a chip to reopen it after a choice.
- [Banner Countdown Sale](https://pixelpapa.com/components/banners/banner-countdown-sale.md) (Pro): A sale strip with a ticking countdown whose digits roll over, a sweeping sheen, and a promo code that confirms itself when copied.
- [Banner Status Incident](https://pixelpapa.com/components/banners/banner-status-incident.md) (Pro): A status strip with a pulsing severity dot that expands on a spring into a timeline of incident updates, recoloured by severity.
- [Banner Upgrade Progress](https://pixelpapa.com/components/banners/banner-upgrade-progress.md): An in-app usage banner whose meter springs to the current value, counts the number up and shifts from calm to amber to red as the plan limit nears.
