CTA Newsletter Section
Banda de suscripción al newsletter con beneficios, campo etiquetado, validación y nota de privacidad.
ctanewsletteremailform
Vista Previa en Vivo
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
Uso
Copia y pega este código directamente en tu página de 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"
/> Instalación Manual
Si no estás usando el paquete npm, crea un archivo nuevo y pega el siguiente código: 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>