1 звезда на GitHub = 1 новый компонент.

Поставить звезду
Призывы к действию (CTA)

CTA Newsletter Section

Блок подписки на рассылку с преимуществами, полем email, валидацией и заметкой о приватности.

ctanewsletteremailform
Живое превью

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.

Использование

Скопируйте и вставьте этот код на вашу страницу Astro:

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" />

Ручная установка

Если вы не используете пакет npm, создайте файл и вставьте следующий код: 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>