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 componente React:
tsx
import { CtaNewsletterSection } from 'react-component-kit-ui/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.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; }
}