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 Astro page:

astro
---
import { CtaNewsletterSection } from 'astro-component-kit/sections';
---

<CtaNewsletterSection
  title="Get new components every week"
  action="/api/subscribe"
  variant="glass"
/>
--- import { CtaNewsletterSection } from 'astro-component-kit/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.astro:

astro
---
/**
 * 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.
 */
interface Props {
  title?: string;
  subtitle?: string;
  perks?: string[];
  action?: string;
  buttonLabel?: string;
  privacy?: string;
  variant?: 'gradient' | 'glass' | 'bordered';
  size?: 'sm' | 'md' | 'lg';
  className?: string;
}

const {
  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 = '',
} = Astro.props;

const id = `cns-${Math.random().toString(36).slice(2, 8)}`;
---

<div class="cq-cta-newsletter-section-cq">
<section class={`cns cns--${variant} cns--${size} ${className}`} aria-labelledby={id}>
  <div class="cns__box">
    <div class="cns__copy">
      <span class="cns__icon" aria-hidden="true">
        <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="4" width="20" height="16" rx="2" /><path d="m22 7-10 6L2 7" /></svg>
      </span>
      <h2 id={id} class="cns__title">{title}</h2>
      <p class="cns__subtitle">{subtitle}</p>
      <ul class="cns__perks">
        {perks.map((p) => (
          <li><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 6 9 17l-5-5" /></svg>{p}</li>
        ))}
      </ul>
    </div>
    <form class="cns__form" action={action} method="post" novalidate>
      <label class="cns__label" for={`${id}-email`}>Email address</label>
      <div class="cns__field">
        <input class="cns__input" id={`${id}-email`} name="email" type="email" required autocomplete="email" placeholder="you@company.com" aria-describedby={`${id}-msg ${id}-privacy`} />
        <button class="cns__btn" type="submit">{buttonLabel}</button>
      </div>
      <p id={`${id}-msg`} class="cns__msg" role="status" aria-live="polite"></p>
      <p id={`${id}-privacy`} class="cns__privacy">{privacy}</p>
    </form>
  </div>
</section>
</div>
<script>
  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 = '';
    });
  });
</script>

<style>
  .cq-cta-newsletter-section-cq { container-type: inline-size; }
  .cns {
    --cns-py: 5rem;
    padding: var(--cns-py) var(--sp-6, 1.5rem);
    background: var(--c-bg, #050505);
  }
  .cns--sm { --cns-py: 3rem; }
  .cns--lg { --cns-py: 7rem; }
  .cns__box {
    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 .cns__box {
    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 .cns__box { 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 .cns__box { border: 1px solid rgb(var(--ink-rgb, 255 255 255) / 0.14); }
  .cns__icon { 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 { 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 { margin: 0 0 1.25rem; font-size: 1rem; line-height: 1.6; color: var(--c-text-2, #a3a3a3); }
  .cns__perks { list-style: none; margin: 0; padding: 0; display: flex; flex-wrap: wrap; gap: 0.5rem 1.25rem; }
  .cns__perks li { display: inline-flex; align-items: center; gap: 0.4rem; font-size: 0.875rem; color: var(--c-text-1, #fff); }
  .cns__perks svg { color: var(--chart-pos, #34c38f); }
  .cns__label { display: block; margin-bottom: 0.5rem; font-size: 0.875rem; font-weight: 600; color: var(--c-text-1, #fff); }
  .cns__field { display: flex; gap: 0.5rem; }
  .cns__input {
    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::placeholder { color: var(--c-text-3, #737373); }
  .cns__input:focus { 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'] { border-color: var(--chart-neg, #f06a6a); }
  .cns__btn {
    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 { background: var(--c-accent, #fef3c7); }
  .cns__btn:focus-visible { outline: 2px solid var(--c-primary-light, #d4a017); outline-offset: 2px; }
  .cns__msg { min-height: 1.4rem; margin: 0.6rem 0 0; font-size: 0.875rem; }
  .cns__msg[data-state='error'] { color: #ff9b9b; }
  .cns__msg[data-state='ok'] { color: #7ee2b8; }
  .cns__privacy { margin: 0.25rem 0 0; font-size: 0.8125rem; color: var(--c-text-3, #8a8a8a); }
  @container (max-width: 820px) {
    .cns__box { grid-template-columns: 1fr; }
  }
  @container (max-width: 460px) {
    .cns__field { flex-direction: column; }
  }
</style>
--- /** * 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. */ interface Props { title?: string; subtitle?: string; perks?: string[]; action?: string; buttonLabel?: string; privacy?: string; variant?: 'gradient' | 'glass' | 'bordered'; size?: 'sm' | 'md' | 'lg'; className?: string; } const { 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 = '', } = Astro.props; const id = `cns-${Math.random().toString(36).slice(2, 8)}`; --- <div class="cq-cta-newsletter-section-cq"> <section class={`cns cns--${variant} cns--${size} ${className}`} aria-labelledby={id}> <div class="cns__box"> <div class="cns__copy"> <span class="cns__icon" aria-hidden="true"> <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="4" width="20" height="16" rx="2" /><path d="m22 7-10 6L2 7" /></svg> </span> <h2 id={id} class="cns__title">{title}</h2> <p class="cns__subtitle">{subtitle}</p> <ul class="cns__perks"> {perks.map((p) => ( <li><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 6 9 17l-5-5" /></svg>{p}</li> ))} </ul> </div> <form class="cns__form" action={action} method="post" novalidate> <label class="cns__label" for={`${id}-email`}>Email address</label> <div class="cns__field"> <input class="cns__input" id={`${id}-email`} name="email" type="email" required autocomplete="email" placeholder="you@company.com" aria-describedby={`${id}-msg ${id}-privacy`} /> <button class="cns__btn" type="submit">{buttonLabel}</button> </div> <p id={`${id}-msg`} class="cns__msg" role="status" aria-live="polite"></p> <p id={`${id}-privacy`} class="cns__privacy">{privacy}</p> </form> </div> </section> </div> <script> 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 = ''; }); }); </script> <style> .cq-cta-newsletter-section-cq { container-type: inline-size; } .cns { --cns-py: 5rem; padding: var(--cns-py) var(--sp-6, 1.5rem); background: var(--c-bg, #050505); } .cns--sm { --cns-py: 3rem; } .cns--lg { --cns-py: 7rem; } .cns__box { 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 .cns__box { 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 .cns__box { 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 .cns__box { border: 1px solid rgb(var(--ink-rgb, 255 255 255) / 0.14); } .cns__icon { 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 { 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 { margin: 0 0 1.25rem; font-size: 1rem; line-height: 1.6; color: var(--c-text-2, #a3a3a3); } .cns__perks { list-style: none; margin: 0; padding: 0; display: flex; flex-wrap: wrap; gap: 0.5rem 1.25rem; } .cns__perks li { display: inline-flex; align-items: center; gap: 0.4rem; font-size: 0.875rem; color: var(--c-text-1, #fff); } .cns__perks svg { color: var(--chart-pos, #34c38f); } .cns__label { display: block; margin-bottom: 0.5rem; font-size: 0.875rem; font-weight: 600; color: var(--c-text-1, #fff); } .cns__field { display: flex; gap: 0.5rem; } .cns__input { 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::placeholder { color: var(--c-text-3, #737373); } .cns__input:focus { 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'] { border-color: var(--chart-neg, #f06a6a); } .cns__btn { 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 { background: var(--c-accent, #fef3c7); } .cns__btn:focus-visible { outline: 2px solid var(--c-primary-light, #d4a017); outline-offset: 2px; } .cns__msg { min-height: 1.4rem; margin: 0.6rem 0 0; font-size: 0.875rem; } .cns__msg[data-state='error'] { color: #ff9b9b; } .cns__msg[data-state='ok'] { color: #7ee2b8; } .cns__privacy { margin: 0.25rem 0 0; font-size: 0.8125rem; color: var(--c-text-3, #8a8a8a); } @container (max-width: 820px) { .cns__box { grid-template-columns: 1fr; } } @container (max-width: 460px) { .cns__field { flex-direction: column; } } </style>