Ir al contenido
Evaluar mi proyecto
Componentes · SeccionesDisponible

Suscripción claymorfismo

Correo en una línea con validación, error junto al campo y botón que se transforma en una confirmación con check.

Una carta al mes. Sin ruido.

Ideas prácticas para operar mejor. Te das de baja con un clic.

Código completo

Es exactamente el archivo que se descarga. Cópialo a tu proyecto y úsalo.

seccion-newsletter.tsx81 líneas
Descargar
"use client";

import { useId, useState, type CSSProperties, type FormEvent } from "react";

/** Suscripción a boletín en una línea: valida el correo al enviar, muestra el error junto al campo y el botón se transforma en una confirmación con check. Claymorfismo. React + TypeScript, sin dependencias. */
export type SeccionNewsletterProps = {
  title?: string;
  description?: string;
  onSubscribe?: (email: string) => void;
  className?: string;
  style?: CSSProperties;
};

const CSS = `
.gcsnw-root { box-sizing: border-box; container-type: inline-size; width: 100%; padding: clamp(24px, 5cqi, 64px) clamp(16px, 4cqi, 56px); background: #fbeee4; color: #40284d; font: 700 16px/1.45 ui-rounded, "SF Pro Rounded", "Nunito", ui-sans-serif, system-ui, sans-serif; }
.gcsnw-root *, .gcsnw-root *::before, .gcsnw-root *::after { box-sizing: border-box; }
.gcsnw-root input, .gcsnw-root button { font: inherit; }
.gcsnw-root :focus-visible { outline: 3px solid #40284d; outline-offset: 3px; }
@media (prefers-reduced-motion: reduce) { .gcsnw-root * { animation: none !important; transition-duration: .01ms !important; } }
.gcsnw-card { display: grid; grid-template-columns: minmax(0, 1.1fr) minmax(0, 1fr); gap: 28px; align-items: center; max-width: 880px; margin: 0 auto; padding: clamp(22px, 4cqi, 44px); border-radius: 44px; background: #ffd0b8; box-shadow: inset -10px -14px 24px rgba(190, 90, 60, .32), inset 10px 14px 24px rgba(255, 255, 255, .6), 18px 24px 40px -14px rgba(190, 90, 60, .4); }
.gcsnw-title { margin: 0 0 6px; font: 900 clamp(26px, 3.8cqi, 40px)/1.05 ui-rounded, "SF Pro Rounded", "Nunito", ui-sans-serif, sans-serif; letter-spacing: -.03em; text-wrap: balance; }
.gcsnw-desc { margin: 0; color: #6d3f43; font-weight: 600; }
.gcsnw-form { display: grid; gap: 10px; }
.gcsnw-row { display: flex; gap: 10px; padding: 8px; border-radius: 999px; background: #ffe6d8; box-shadow: inset 5px 6px 10px rgba(190, 90, 60, .3), inset -5px -6px 10px rgba(255, 255, 255, .8); }
.gcsnw-row[data-e="true"] { box-shadow: inset 5px 6px 10px rgba(190, 90, 60, .3), inset -5px -6px 10px rgba(255, 255, 255, .8), 0 0 0 3px #c2263a; }
.gcsnw-in { flex: 1; width: 100%; min-width: 0; padding: 0 16px; border: 0; background: transparent; color: inherit; outline: 0; }
.gcsnw-in::placeholder { color: #a9766f; font-weight: 600; }
.gcsnw-go { position: relative; display: inline-flex; align-items: center; justify-content: center; gap: 8px; min-width: 130px; min-height: 52px; padding: 0 22px; border: 0; border-radius: 999px; background: #40284d; color: #fff; font-weight: 800; cursor: pointer; box-shadow: inset -4px -5px 8px rgba(0, 0, 0, .4), inset 4px 5px 8px rgba(255, 255, 255, .22), 4px 8px 12px -4px rgba(64, 40, 77, .55); transition: transform .3s cubic-bezier(.34, 1.56, .64, 1), background .3s, min-width .4s cubic-bezier(.16, 1, .3, 1); }
.gcsnw-go:hover { transform: scale(1.04); }
.gcsnw-go:active { transform: scale(.95, .9); }
.gcsnw-go[data-ok="true"] { background: #1f9d6b; }
.gcsnw-go svg { stroke-dasharray: 24; stroke-dashoffset: 24; animation: gcsnw-draw .5s ease-out .1s forwards; }
.gcsnw-msg { min-height: 20px; margin: 0 0 0 16px; font-size: 14px; color: #c2263a; }
.gcsnw-msg[data-ok="true"] { color: #12603f; }
@keyframes gcsnw-draw { to { stroke-dashoffset: 0; } }
@container (max-width: 680px) { .gcsnw-card { grid-template-columns: minmax(0, 1fr); } }`;

export function SeccionNewsletter({ title = "Una carta al mes. Sin ruido.", description = "Ideas prácticas para operar mejor. Te das de baja con un clic.", onSubscribe, className = "", style }: SeccionNewsletterProps) {
  const id = useId();
  const [email, setEmail] = useState("");
  const [state, setState] = useState<"idle" | "error" | "ok">("idle");

  function submit(event: FormEvent) {
    event.preventDefault();
    if (!/^\S+@\S+\.\S+$/.test(email.trim())) { setState("error"); return; }
    onSubscribe?.(email.trim());
    setState("ok");
  }

  return (
    <section className={`gcsnw-root ${className}`} style={style}>
      <style>{CSS}</style>
      <div className="gcsnw-card">
        <div>
          <h2 className="gcsnw-title">{title}</h2>
          <p className="gcsnw-desc">{description}</p>
        </div>
        <form className="gcsnw-form" onSubmit={submit} noValidate>
          <div className="gcsnw-row" data-e={state === "error"}>
            <label htmlFor={id} style={{ position: "absolute", width: 1, height: 1, overflow: "hidden", clip: "rect(0 0 0 0)" }}>Correo electrónico</label>
            <input id={id} className="gcsnw-in" type="email" placeholder="tu@correo.com" value={email} disabled={state === "ok"} aria-invalid={state === "error"} aria-describedby={`${id}-m`} onChange={(event) => { setEmail(event.target.value); if (state === "error") setState("idle"); }} />
            <button className="gcsnw-go" type="submit" data-ok={state === "ok"} disabled={state === "ok"}>
              {state === "ok" ? (
                <>
                  <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"><path d="m5 12.5 4.5 4.5L19 7.5" /></svg>
                  Listo
                </>
              ) : (
                "Suscribirme"
              )}
            </button>
          </div>
          <p className="gcsnw-msg" id={`${id}-m`} role="status" data-ok={state === "ok"}>
            {state === "error" ? "Escribe un correo válido, por ejemplo nombre@dominio.com." : state === "ok" ? "Gracias. Revisa tu bandeja para confirmar." : ""}
          </p>
        </form>
      </div>
    </section>
  );
}

Úsalo en tu proyecto

Instálalo con la CLI de shadcn.

terminal.sh1 líneas
npx shadcn@latest add https://www.gassierconsulting.com/r/seccion-newsletter

Ejemplo de uso de Suscripción claymorfismo

Después de copiar seccion-newsletter.tsx a tu proyecto, impórtalo y colócalo donde lo necesites.

ejemplo.tsx5 líneas
import { SeccionNewsletter } from "./seccion-newsletter";

export default function Ejemplo() {
  return <SeccionNewsletter title="Ejemplo" />;
}

Propiedades de SeccionNewsletter

SeccionNewsletter acepta 5 propiedades: todas son opcionales y tienen un valor por defecto, así que funciona sin configurar nada.

PropiedadTipoDescripción
title?stringOpcional.
description?stringOpcional.
onSubscribe?(email: string) => voidOpcional.
className?stringOpcional.
style?CSSPropertiesOpcional.

Accesibilidad y rendimiento

El archivo tiene 81 líneas (6 KB) y define 1 animación con @keyframes. Esto es lo que hace Suscripción claymorfismo por quien lo usa:

  • Detecta prefers-reduced-motion y baja o quita el movimiento cuando la persona lo pide.
  • Muestra un anillo de foco visible al navegar con teclado (:focus-visible).
  • Declara roles ARIA para lectores de pantalla: status.
  • Es un componente de cliente de Next.js: incluye la directiva "use client".
Licencia
MIT
Dependencias
React >=18
Compatibilidad
React 18 y 19 · Next.js (componente de cliente)