Page Sections ctanewsletteremailform
CTA Newsletter Section
Newsletter signup band with benefits, labelled email field, inline validation and privacy note.
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
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/lib/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>
Quick Info
- Category
- Page Sections
- Filename
CTANewsletterSection.astro- Dependencies
- None — pure Astro + CSS
- Tags
- ctanewsletteremailform