# Newsletter Card Perks — React Contact Section

> A signup card with a staggered perks list on one side and a fanned stack of past issues that spreads apart on hover on the other.

- Category: [Contact & Newsletter](https://pixelpapa.com/components/contact.md)
- Tags: newsletter, card, perks, stack, hover
- Dependencies: lucide-react, motion (plus React and Tailwind CSS v4)
- Last updated: 2026-11-13
- Live preview: https://pixelpapa.com/components/contact/newsletter-card-perks

## Install

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

The file is written to `components/pixelpapa/newsletter-card-perks.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 { ArrowRight, BookOpen, Check, Gift, Sparkles, Users } from "lucide-react";
import { motion } from "motion/react";
import { useState, type FormEvent } from "react";

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

const perks = [
  { icon: BookOpen, title: "One deep dive a week", body: "A 1,200-word teardown of a pricing page that works." },
  { icon: Gift, title: "The swipe file", body: "140 annotated screenshots, yours on signup." },
  { icon: Users, title: "Members-only office hours", body: "Bring your page, leave with three fixes." },
  { icon: Sparkles, title: "Early access", body: "Templates and benchmarks before anyone else." },
];

const issues = [
  { no: 61, title: "Why the middle plan always wins", tone: ["var(--chart-3)", "var(--chart-1)"], rest: { rotate: -9, x: -34, y: 14 }, fan: { rotate: -16, x: -92, y: 22 } },
  { no: 62, title: "Annual discounts: 17% is not a law", tone: ["var(--chart-2)", "var(--chart-5)"], rest: { rotate: 7, x: 30, y: 8 }, fan: { rotate: 14, x: 92, y: 18 } },
  { no: 63, title: "The free tier that pays for itself", tone: ["var(--chart-1)", "var(--chart-2)"], rest: { rotate: -1, x: 0, y: 0 }, fan: { rotate: 0, x: 0, y: -14 } },
];

export default function NewsletterCardPerks() {
  const [email, setEmail] = useState("");
  const [joined, setJoined] = useState(false);

  function onSubmit(e: FormEvent) {
    e.preventDefault();
    if (email.includes("@")) setJoined(true);
  }

  return (
    <section className="w-full bg-background px-6 py-20">
      <div className="mx-auto grid max-w-5xl overflow-hidden rounded-[2rem] border border-border bg-card shadow-2xl shadow-primary/5 lg:grid-cols-[1.15fr_1fr]">
        <div className="p-8 sm:p-12">
          <p className="text-xs font-medium tracking-[0.18em] text-primary uppercase">Price Tag Weekly</p>
          <h2 className="mt-4 text-3xl font-semibold tracking-tight text-balance text-card-foreground sm:text-4xl">
            Pricing lessons from 300 SaaS teams, in your inbox on Thursdays.
          </h2>

          <motion.ul
            initial="hidden"
            whileInView="shown"
            viewport={{ once: true, amount: 0.4 }}
            transition={{ staggerChildren: 0.08, delayChildren: 0.15 }}
            className="mt-8 grid gap-5 sm:grid-cols-2"
          >
            {perks.map(({ icon: Icon, title, body }) => (
              <motion.li
                key={title}
                variants={{ hidden: { opacity: 0, y: 14 }, shown: { opacity: 1, y: 0, transition: { duration: 0.5, ease } } }}
                className="flex gap-3"
              >
                <span className="mt-0.5 flex size-8 shrink-0 items-center justify-center rounded-lg bg-primary/10 text-primary">
                  <Icon className="size-4" />
                </span>
                <span className="text-sm">
                  <span className="block font-medium text-card-foreground">{title}</span>
                  <span className="text-muted-foreground">{body}</span>
                </span>
              </motion.li>
            ))}
          </motion.ul>

          <form onSubmit={onSubmit} className="mt-10">
            {joined ? (
              <motion.p
                role="status"
                initial={{ opacity: 0, y: 10 }}
                animate={{ opacity: 1, y: 0 }}
                className="flex h-12 items-center gap-3 rounded-xl bg-emerald-500/10 px-4 text-sm font-medium text-emerald-700 dark:text-emerald-300"
              >
                <span className="flex size-6 items-center justify-center rounded-full bg-emerald-500 text-white">
                  <Check className="size-3.5" strokeWidth={3} />
                </span>
                Welcome aboard. The swipe file is in your inbox.
              </motion.p>
            ) : (
              <div className="flex flex-col gap-2 sm:flex-row">
                <label htmlFor="newsletter-card-perks-email" className="sr-only">
                  Email address
                </label>
                <input
                  id="newsletter-card-perks-email"
                  type="email"
                  required
                  value={email}
                  onChange={(e) => setEmail(e.target.value)}
                  placeholder="name@company.com"
                  className="h-12 min-w-0 flex-1 rounded-xl border border-input bg-background px-4 text-sm text-foreground outline-none transition-shadow placeholder:text-muted-foreground focus:border-primary focus:ring-4 focus:ring-primary/15"
                />
                <button
                  type="submit"
                  className="group inline-flex h-12 items-center justify-center gap-2 rounded-xl bg-primary px-6 text-sm font-medium text-primary-foreground shadow-lg shadow-primary/25"
                >
                  Join free
                  <ArrowRight className="size-4 transition-transform group-hover:translate-x-0.5" />
                </button>
              </div>
            )}
            <p className="mt-3 text-xs text-muted-foreground">Free forever. 41% open rate, which tells you nobody is being spammed.</p>
          </form>
        </div>

        <motion.div
          initial="rest"
          whileHover="fan"
          className="relative flex min-h-80 items-center justify-center overflow-hidden border-t border-border bg-muted/50 lg:border-t-0 lg:border-l"
        >
          <div aria-hidden className="absolute inset-0 [background-image:radial-gradient(var(--border)_1.2px,transparent_1.2px)] [background-size:18px_18px] [mask-image:radial-gradient(60%_60%_at_50%_50%,black,transparent)]" />
          {issues.map((issue) => (
            <motion.article
              key={issue.no}
              variants={{ rest: issue.rest, fan: issue.fan }}
              transition={{ type: "spring", stiffness: 220, damping: 20 }}
              className="absolute flex h-64 w-48 flex-col justify-between rounded-2xl p-5 text-white shadow-2xl ring-1 ring-white/20"
              style={{ background: `linear-gradient(150deg, ${issue.tone[0]}, ${issue.tone[1]})` }}
            >
              <div className="flex items-center justify-between text-[11px] font-medium tracking-wider uppercase opacity-80">
                <span>Issue</span>
                <span className="font-mono">№ {issue.no}</span>
              </div>
              <svg aria-hidden viewBox="0 0 120 60" className="w-full opacity-70" fill="none" stroke="white" strokeWidth="1.5">
                <path d={`M0 ${40 + (issue.no % 3) * 5} C 30 ${10 + (issue.no % 2) * 20}, 50 55, 80 ${24 - (issue.no % 3) * 4} S 110 20, 120 ${8 + (issue.no % 2) * 10}`} />
                <path d="M0 59.5h120" strokeOpacity="0.4" />
              </svg>
              <p className="text-lg leading-tight font-semibold tracking-tight text-balance">{issue.title}</p>
            </motion.article>
          ))}
        </motion.div>
      </div>
    </section>
  );
}
```

## AI prompt

```text
Add the "Newsletter Card Perks" component from PixelPapa UI to this project.

What it is: A signup card with a staggered perks list on one side and a fanned stack of past issues that spreads apart on hover on the other.
Category: Contact & Newsletter. Depends on: lucide-react, motion.

1. Install it with the shadcn CLI:
   npx shadcn@latest add "https://pixelpapa.com/r/newsletter-card-perks.json"
   (If the CLI is unavailable, fetch that URL, write files[0].content to components/pixelpapa/newsletter-card-perks.tsx and install the listed dependencies.)
2. Import it from "@/components/pixelpapa/newsletter-card-perks" 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 contact & newsletter

- [Contact Booking Slots](https://pixelpapa.com/components/contact/contact-booking-slots.md) (Pro): A meeting scheduler card: pick a day from a strip, then a time slot that highlights with a shared-layout pill, switch 12/24-hour format and confirm into a summary.
- [Contact Cards Channels](https://pixelpapa.com/components/contact/contact-cards-channels.md): Three contact channel cards, each with its own live detail: a copy-to-clipboard email, a chat thread with a typing indicator and a voice line with moving level bars.
- [Contact Map Office](https://pixelpapa.com/components/contact/contact-map-office.md) (Pro): A dotted SVG world map with pulsing office pins linked by animated arcs; the office list and the pins select each other and show live local time.
- [Contact Split Form](https://pixelpapa.com/components/contact/contact-split-form.md) (Pro): Two-column contact section whose form has floating labels, inline validation that slides in on blur, a shake on invalid submit and a drawn-check success state.
- [Newsletter Inline Success](https://pixelpapa.com/components/contact/newsletter-inline-success.md): A single-line signup whose submit button shrinks into a spinner, then a green check, while the field collapses around a confirmation message.
