1 GitHub star = 1 new component.

Star the repo
Call to Action

CTA Newsletter Section

Newsletter signup band with benefits, labelled email field, inline validation and privacy note.

ctanewsletteremailform
Live Preview

Get new components every week

One short email with fresh sections, charts and Astro tips. Join 12,000+ developers.

  • Weekly, never spam
  • Early access to new blocks
  • Unsubscribe in one click

We respect your privacy. Read our privacy policy.

Usage

Copy and paste this code into your React component:

tsx
import { CtaNewsletterSection } from 'react-component-kit-ui/sections';

<CtaNewsletterSection title="Get new components every week" action="/api/subscribe" variant="glass" />

Manual Installation

If you are not using the npm package, create a new file and paste the following code: src/components/sections/CtaNewsletterSection.tsx + CtaNewsletterSection.css:

CtaNewsletterSection.tsx
'use client';

import { useEffect, useRef } from 'react';
import './CtaNewsletterSection.css';

/**
 * CtaNewsletterSection โ€” Newsletter signup band with benefits list, accessible
 * email form, inline validation and privacy note.
 *
 * @param {string} title - Heading.
 * @param {string} subtitle - Supporting copy.
 * @param {string[]} perks - Short benefit bullets.
 * @param {string} action - Form action URL.
 * @param {string} buttonLabel - Submit label.
 * @param {string} privacy - Privacy note under the form.
 * @param {string} variant - Band style (gradient|glass|bordered).
 * @param {string} size - Vertical rhythm (sm|md|lg).
 * @param {string} className - Extra CSS class.
 */
export interface CtaNewsletterSectionProps {
  title?: string;
  subtitle?: string;
  perks?: string[];
  action?: string;
  buttonLabel?: string;
  privacy?: string;
  variant?: 'gradient' | 'glass' | 'bordered';
  size?: 'sm' | 'md' | 'lg';
  className?: string;
}

export function CtaNewsletterSection({
  title = 'Get new components every week',
  subtitle = 'One short email with fresh sections, charts and Astro tips. Join 12,000+ developers.',
  perks = ['Weekly, never spam', 'Early access to new blocks', 'Unsubscribe in one click'],
  action = '#',
  buttonLabel = 'Subscribe',
  privacy = 'We respect your privacy. Read our privacy policy.',
  variant = 'gradient',
  size = 'md',
  className = '',
}: CtaNewsletterSectionProps) {
  const id = `cns-${Math.random().toString(36).slice(2, 8)}`;

  // Ported from the Astro <script>: runs once after mount (DOM logic kept as in the Astro kit).
  const mounted = useRef(false);
  useEffect(() => {
    if (mounted.current) return;
    mounted.current = true;
    document.querySelectorAll<HTMLFormElement>('.cns__form').forEach((form) => {
        const input = form.querySelector<HTMLInputElement>('.cns__input');
        const msg = form.querySelector<HTMLElement>('.cns__msg');
        form.addEventListener('submit', (e) => {
          e.preventDefault();
          if (!input || !msg) return;
          if (!input.value || !input.checkValidity()) {
            input.setAttribute('aria-invalid', 'true');
            msg.dataset.state = 'error';
            msg.textContent = 'Please enter a valid email address.';
            input.focus();
            return;
          }
          input.removeAttribute('aria-invalid');
          msg.dataset.state = 'ok';
          msg.textContent = 'Thanks! Please confirm your subscription in your inbox.';
          input.value = '';
        });
      });
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  return (
    <>
      <div className="cq-cta-newsletter-section-cq" data-rk-95ed6a="">
      <section className={`cns cns--${variant} cns--${size} ${className}`} aria-labelledby={id} data-rk-95ed6a="">
        <div className="cns__box" data-rk-95ed6a="">
          <div className="cns__copy" data-rk-95ed6a="">
            <span className="cns__icon" aria-hidden="true" data-rk-95ed6a="">
              <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" data-rk-95ed6a=""><rect x="2" y="4" width="20" height="16" rx="2" data-rk-95ed6a="" /><path d="m22 7-10 6L2 7" data-rk-95ed6a="" /></svg>
            </span>
            <h2 id={id} className="cns__title" data-rk-95ed6a="">{title}</h2>
            <p className="cns__subtitle" data-rk-95ed6a="">{subtitle}</p>
            <ul className="cns__perks" data-rk-95ed6a="">
              {perks.map((p, __i: number) => (
                <li key={__i} data-rk-95ed6a=""><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" data-rk-95ed6a=""><path d="M20 6 9 17l-5-5" data-rk-95ed6a="" /></svg>{p}</li>
              ))}
            </ul>
          </div>
          <form className="cns__form" action={action} method="post" noValidate data-rk-95ed6a="">
            <label className="cns__label" htmlFor={`${id}-email`} data-rk-95ed6a="">Email address</label>
            <div className="cns__field" data-rk-95ed6a="">
              <input className="cns__input" id={`${id}-email`} name="email" type="email" required autoComplete="email" placeholder="you@company.com" aria-describedby={`${id}-msg ${id}-privacy`} data-rk-95ed6a="" />
              <button className="cns__btn" type="submit" data-rk-95ed6a="">{buttonLabel}</button>
            </div>
            <p id={`${id}-msg`} className="cns__msg" role="status" aria-live="polite" data-rk-95ed6a=""></p>
            <p id={`${id}-privacy`} className="cns__privacy" data-rk-95ed6a="">{privacy}</p>
          </form>
        </div>
      </section>
      </div>
    </>
  );
}

export default CtaNewsletterSection;
CtaNewsletterSection.css
/* CtaNewsletterSection โ€” ported from astro-component-kit (sections/CtaNewsletterSection.astro) */

.cq-cta-newsletter-section-cq:where([data-rk-95ed6a]) { container-type: inline-size; }
.cns:where([data-rk-95ed6a]) {
  --cns-py: 5rem;
  padding: var(--cns-py) var(--sp-6, 1.5rem);
  background: var(--c-bg, #050505);
}
.cns--sm:where([data-rk-95ed6a]) { --cns-py: 3rem; }
.cns--lg:where([data-rk-95ed6a]) { --cns-py: 7rem; }
.cns__box:where([data-rk-95ed6a]) {
  max-width: 1080px;
  margin: 0 auto;
  display: grid;
  grid-template-columns: 1.2fr 1fr;
  gap: 2.5rem;
  align-items: center;
  padding: clamp(2rem, 5vw, 3.5rem);
  border-radius: var(--r-xl, 28px);
}
.cns--gradient:where([data-rk-95ed6a]) .cns__box:where([data-rk-95ed6a]) {
  background: radial-gradient(circle at 100% 0%, rgba(144, 133, 233, 0.22), transparent 55%), radial-gradient(circle at 0% 100%, var(--c-primary-glow, color-mix(in srgb, var(--c-primary, #b8860b) 20%, transparent)), transparent 55%), #0c0c10;
  border: 1px solid rgb(var(--ink-rgb, 255 255 255) / 0.08);
}
.cns--glass:where([data-rk-95ed6a]) .cns__box:where([data-rk-95ed6a]) { background: rgb(var(--ink-rgb, 255 255 255) / 0.035); border: 1px solid var(--c-border, rgb(var(--ink-rgb, 255 255 255) / 0.08)); backdrop-filter: blur(16px); }
.cns--bordered:where([data-rk-95ed6a]) .cns__box:where([data-rk-95ed6a]) { border: 1px solid rgb(var(--ink-rgb, 255 255 255) / 0.14); }
.cns__icon:where([data-rk-95ed6a]) { display: grid; place-items: center; width: 48px; height: 48px; margin-bottom: 1.25rem; border-radius: 14px; background: rgb(var(--ink-rgb, 255 255 255) / 0.06); border: 1px solid rgb(var(--ink-rgb, 255 255 255) / 0.1); color: var(--c-text-1, #fff); }
.cns__title:where([data-rk-95ed6a]) { margin: 0 0 0.75rem; font-size: clamp(1.6rem, 3.5vw, 2.25rem); font-weight: 800; letter-spacing: -0.03em; line-height: 1.15; color: var(--c-text-1, #fff); }
.cns__subtitle:where([data-rk-95ed6a]) { margin: 0 0 1.25rem; font-size: 1rem; line-height: 1.6; color: var(--c-text-2, #a3a3a3); }
.cns__perks:where([data-rk-95ed6a]) { list-style: none; margin: 0; padding: 0; display: flex; flex-wrap: wrap; gap: 0.5rem 1.25rem; }
.cns__perks:where([data-rk-95ed6a]) li:where([data-rk-95ed6a]) { display: inline-flex; align-items: center; gap: 0.4rem; font-size: 0.875rem; color: var(--c-text-1, #fff); }
.cns__perks:where([data-rk-95ed6a]) svg:where([data-rk-95ed6a]) { color: var(--chart-pos, #34c38f); }
.cns__label:where([data-rk-95ed6a]) { display: block; margin-bottom: 0.5rem; font-size: 0.875rem; font-weight: 600; color: var(--c-text-1, #fff); }
.cns__field:where([data-rk-95ed6a]) { display: flex; gap: 0.5rem; }
.cns__input:where([data-rk-95ed6a]) {
  flex: 1;
  min-width: 0;
  min-height: 50px;
  padding: 0 1rem;
  border-radius: var(--r-md, 12px);
  border: 1px solid rgb(var(--ink-rgb, 255 255 255) / 0.14);
  background: rgba(0, 0, 0, 0.35);
  color: var(--c-text-1, #fff);
  font: inherit;
  font-size: 1rem;
  transition: border-color 0.2s ease, box-shadow 0.2s ease;
}
.cns__input:where([data-rk-95ed6a])::placeholder { color: var(--c-text-3, #737373); }
.cns__input:focus:where([data-rk-95ed6a]) { outline: none; border-color: var(--c-primary-light, #d4a017); box-shadow: 0 0 0 3px var(--c-primary-glow, color-mix(in srgb, var(--c-primary, #b8860b) 25%, transparent)); }
.cns__input[aria-invalid='true']:where([data-rk-95ed6a]) { border-color: var(--chart-neg, #f06a6a); }
.cns__btn:where([data-rk-95ed6a]) {
  min-height: 50px;
  padding: 0 1.4rem;
  border: 0;
  border-radius: var(--r-md, 12px);
  background: var(--c-text-1, #fff);
  color: var(--c-on-primary, #0a0a0a);
  font: inherit;
  font-weight: 650;
  cursor: pointer;
  transition: background 0.2s ease;
}
.cns__btn:hover:where([data-rk-95ed6a]) { background: var(--c-accent, #fef3c7); }
.cns__btn:focus-visible:where([data-rk-95ed6a]) { outline: 2px solid var(--c-primary-light, #d4a017); outline-offset: 2px; }
.cns__msg:where([data-rk-95ed6a]) { min-height: 1.4rem; margin: 0.6rem 0 0; font-size: 0.875rem; }
.cns__msg[data-state='error']:where([data-rk-95ed6a]) { color: #ff9b9b; }
.cns__msg[data-state='ok']:where([data-rk-95ed6a]) { color: #7ee2b8; }
.cns__privacy:where([data-rk-95ed6a]) { margin: 0.25rem 0 0; font-size: 0.8125rem; color: var(--c-text-3, #8a8a8a); }
@container (max-width: 820px) {
  .cns__box:where([data-rk-95ed6a]) { grid-template-columns: 1fr; }
}
@container (max-width: 460px) {
  .cns__field:where([data-rk-95ed6a]) { flex-direction: column; }
}