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.
"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.
npx shadcn@latest add https://www.gassierconsulting.com/r/seccion-newsletterEjemplo de uso de Suscripción claymorfismo
Después de copiar seccion-newsletter.tsx a tu proyecto, impórtalo y colócalo donde lo necesites.
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.
| Propiedad | Tipo | Descripción |
|---|---|---|
title? | string | Opcional. |
description? | string | Opcional. |
onSubscribe? | (email: string) => void | Opcional. |
className? | string | Opcional. |
style? | CSSProperties | Opcional. |
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".