1 estrella en GitHub = 1 componente nuevo.

Dar una estrella
Llamadas a la Acción

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

We respect your privacy. Read our privacy policy.

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; }
}