More Forms Snippets
Newsletter Signup Card — Animated Success State HTML CSS JS
Newsletter Signup Card · Forms · Plain HTML, CSS & JS · Live preview
What's included
Features
/^[^s@]+@[^s@]+.[^s@]+$/ catches obvious format errors. role="alert" + aria-live="assertive" announces errors to screen readers immediately..loading class fades out button text and shows a centred CSS spinner, keeping button dimensions fixed to prevent layout shift during the async delay.stroke-dashoffset keyframe animations that draw in sequence — circle first (0.6s), then checkmark (0.4s, delayed 0.65s).hidden attribute toggled on submit. The success state uses animation: fadeIn to slide up, avoiding layout jump.<p role="alert"> below the input and add .error-state class (red border). The input listener clears errors on every keystroke — immediate feedback.confirmedEmail.textContent = val) — a small but high-trust detail that confirms the correct address was saved.About this UI Snippet
Newsletter Signup Card — Form Validation, Animated SVG Check & State Machine Pattern

A newsletter signup form is one of the highest-value conversion elements on any website, yet most implementations are a bare <input> and <button> with no success feedback. This snippet builds a polished email signup card: client-side email validation with inline error messages, a loading spinner during the async call, and an animated SVG checkmark success state that slides in after submission — with three confirmation perks and the subscriber's email shown back to them.
The success state is where most implementations fall short. Users who submit a form with no feedback don't know whether it worked. This snippet solves that with a two-state card: an idle state (form) and a success state (animated confirmation). The transition between states uses CSS animation — not a page reload.
Email validation with inline error messages
The form uses novalidate to disable native browser validation, giving us full control over the error UI. The validateEmail function checks the value against /^[^s@]+@[^s@]+.[^s@]+$/ — a pragmatic regex that catches obvious format errors without the complexity of RFC 5322 full compliance (which rejects valid emails). Errors render in a role="alert" aria-live="assertive" paragraph below the input so screen readers announce them immediately. The error state adds a red border to the input via a class toggle.
Loading state with spinner
During the async submission, the button gets a .loading class that fades out the button text and arrow (opacity: 0) and fades in a centred CSS spinner. The spinner is an ::after-style border-radius element with a rotate(360deg) keyframe animation. The button is also disabled to prevent double-submission. This pattern — hiding text but keeping the button the same size — prevents layout shift during loading.
Animated SVG checkmark
The success state shows an SVG circle and checkmark path drawn with CSS stroke-dashoffset animation. The circle stroke is set to stroke-dasharray: 151 (its circumference) and stroke-dashoffset: 151 (fully hidden). A keyframe animation drives it to 0 — drawing the circle. The check path follows 200ms later with the same technique. This two-step sequence (circle then check) creates a satisfying "confirmation" feeling that flat success messages lack.
Two-state card pattern
The idle and success states are both in the DOM, one hidden with the hidden attribute. Submission hides the idle state and removes hidden from the success state. CSS animation: fadeIn .5s ease slides the success state up from 12px below. This is simpler and more reliable than injecting HTML on success, and it means the success state can be styled independently without JavaScript string templates. Pair with a toast notification for site-wide signup confirmation that appears on other pages after redirect.
Step by step
How to Use
- 1Paste HTML, CSS, and JSA newsletter card appears with an email icon, headline, feature badges, email input, and subscribe button on a purple gradient background.
- 2Try submitting emptyClick Subscribe without an email — an inline error message appears below the input with a red border on the field. The error clears as you start typing.
- 3Try an invalid emailEnter "test" and submit — a validation error appears. Enter "[email protected]" and submit to proceed.
- 4Watch the loading stateA spinner appears inside the button for ~1.4 seconds (simulating an API call). The button disables to prevent double submission.
- 5See the success stateAn animated SVG circle draws itself, then a checkmark path draws inside it. The success card slides up with the subscriber's email confirmed and three benefit perks.
- 6Connect your email APIReplace the
await new Promise(r => setTimeout(r, 1400))with your real API call (fetch('/api/subscribe', { method: 'POST', body: ... })). Show the success state in thethenhandler, or re-show the form with an error message if the call fails.
Real-world uses
Common Use Cases
Got questions?
Frequently Asked Questions
Replace the setTimeout with a fetch call to their API endpoint. For Mailchimp: fetch(https://us1.api.mailchimp.com/3.0/lists/{listId}/members, { method:'POST', headers:{Authorization:'apikey ${key}',...}, body: JSON.stringify({email_address:val,status:'subscribed'}) }). For ConvertKit: POST to https://api.convertkit.com/v3/forms/{formId}/subscribe with {api_key, email}.
Add a try/catch around the fetch. On error, re-enable the button, remove the .loading class, and call showError('This email is already subscribed.'). This keeps the user in the idle state with the error inline rather than losing their input.
Add <input type="text" placeholder="First name" /> above the email input. In JS, collect both values and include the name in the API call body. The validation step adds a check that the name field is non-empty.
Use useState for { email, error, loading, success }. The form onSubmit sets loading: true, awaits the API call, then sets success: true or error: 'message'. Conditionally render the idle or success JSX based on the success flag.
Newsletter Signup Card — Export as HTML, React, Vue, Angular & Tailwind
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Newsletter Signup Card</title>
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,sans-serif;background:linear-gradient(135deg,#ede9fe 0%,#e0f2fe 100%);min-height:100vh;display:flex;align-items:center;justify-content:center;padding:20px}
.card{background:#fff;border-radius:24px;padding:36px 32px;max-width:420px;width:100%;box-shadow:0 20px 60px rgba(99,102,241,.12);text-align:center}
/* Idle state */
.icon-wrap{width:52px;height:52px;background:linear-gradient(135deg,#ede9fe,#ddd6fe);border-radius:14px;display:flex;align-items:center;justify-content:center;margin:0 auto 20px;color:#7c3aed}
.title{font-size:22px;font-weight:800;color:#1e293b;margin-bottom:8px}
.desc{font-size:14px;color:#64748b;line-height:1.6;margin-bottom:16px}
.badges{display:flex;gap:8px;justify-content:center;flex-wrap:wrap;margin-bottom:24px}
.badge{font-size:11px;font-weight:600;color:#6366f1;background:#eef2ff;border-radius:20px;padding:4px 10px}
.input-wrap{position:relative;display:flex;align-items:center}
.input-icon{position:absolute;left:13px;color:#94a3b8;pointer-events:none;flex-shrink:0}
.input{width:100%;padding:13px 16px 13px 38px;font-size:14px;border:2px solid #e2e8f0;border-radius:12px;outline:none;color:#1e293b;background:#f8fafc;transition:border-color .2s,background .2s;font-family:inherit}
.input:focus{border-color:#6366f1;background:#fff}
.input.error-state{border-color:#ef4444}
.error-msg{font-size:12px;color:#ef4444;margin-top:6px;min-height:16px;text-align:left}
.btn{display:flex;align-items:center;justify-content:center;gap:8px;width:100%;margin-top:10px;padding:14px;background:linear-gradient(135deg,#6366f1,#8b5cf6);color:#fff;font-size:14px;font-weight:700;border:none;border-radius:12px;cursor:pointer;transition:opacity .2s,transform .15s;position:relative;overflow:hidden;font-family:inherit}
.btn:hover{opacity:.92;transform:translateY(-1px)}
.btn:active{transform:translateY(0)}
.btn.loading .btn-text,.btn.loading .btn-arrow{opacity:0}
.btn.loading .btn-spinner{opacity:1}
.btn-spinner{position:absolute;width:18px;height:18px;border:2.5px solid rgba(255,255,255,.3);border-top-color:#fff;border-radius:50%;animation:spin .7s linear infinite;opacity:0;transition:opacity .2s}
@keyframes spin{to{transform:rotate(360deg)}}
.social-count{font-size:12px;color:#94a3b8;margin-top:14px}
.social-count strong{color:#475569}
/* Success state */
.state-success{animation:fadeIn .5s ease}
@keyframes fadeIn{from{opacity:0;transform:translateY(12px)}to{opacity:1;transform:none}}
.success-ring{width:80px;height:80px;margin:0 auto 20px;color:#10b981}
.check-circle{stroke-dasharray:151;stroke-dashoffset:151;animation:drawCircle .6s .1s ease forwards}
.check-path{stroke-dasharray:40;stroke-dashoffset:40;animation:drawCheck .4s .65s ease forwards}
@keyframes drawCircle{to{stroke-dashoffset:0}}
@keyframes drawCheck{to{stroke-dashoffset:0}}
.success-title{font-size:22px;font-weight:800;color:#1e293b;margin-bottom:8px}
.success-desc{font-size:14px;color:#64748b;line-height:1.6;margin-bottom:24px}
.success-perks{display:flex;flex-direction:column;gap:10px;text-align:left;background:#f0fdf4;border:1px solid #bbf7d0;border-radius:12px;padding:16px}
.perk{display:flex;align-items:center;gap:8px;font-size:13px;color:#166534;font-weight:500}
</style>
</head>
<body>
<div class="page">
<div class="card" id="card">
<div class="state-idle" id="stateIdle">
<div class="icon-wrap">
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/><polyline points="22,6 12,13 2,6"/></svg>
</div>
<h2 class="title">Stay in the loop</h2>
<p class="desc">Get weekly dev tips, UI snippets, and early access to new tools — no spam, ever.</p>
<div class="badges">
<span class="badge">📦 175+ snippets</span>
<span class="badge">⚡ Weekly tips</span>
<span class="badge">🔒 No spam</span>
</div>
<form class="form" id="form" novalidate>
<div class="input-wrap" id="inputWrap">
<svg class="input-icon" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/><polyline points="22,6 12,13 2,6"/></svg>
<input id="emailInput" type="email" class="input" placeholder="[email protected]" autocomplete="email" aria-label="Email address" required />
</div>
<p class="error-msg" id="errorMsg" role="alert" aria-live="assertive"></p>
<button type="submit" class="btn" id="submitBtn">
<span class="btn-text">Subscribe</span>
<svg class="btn-arrow" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><path d="M5 12h14"/><path d="m13 6 6 6-6 6"/></svg>
<span class="btn-spinner" aria-hidden="true"></span>
</button>
</form>
<p class="social-count"><strong>12,847</strong> developers already subscribed</p>
</div>
<div class="state-success" id="stateSuccess" hidden>
<div class="success-ring">
<svg class="success-check" viewBox="0 0 52 52" fill="none">
<circle class="check-circle" cx="26" cy="26" r="24" stroke="currentColor" stroke-width="2.5"/>
<path class="check-path" d="M14 26 L22 34 L38 18" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</div>
<h2 class="success-title">You're subscribed!</h2>
<p class="success-desc">Check <strong id="confirmedEmail">your inbox</strong> for a confirmation. Welcome aboard! 🎉</p>
<div class="success-perks">
<div class="perk"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#10b981" stroke-width="2.5" stroke-linecap="round"><polyline points="20 6 9 17 4 12"/></svg>Weekly dev tips delivered</div>
<div class="perk"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#10b981" stroke-width="2.5" stroke-linecap="round"><polyline points="20 6 9 17 4 12"/></svg>Early access to new UI snippets</div>
<div class="perk"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#10b981" stroke-width="2.5" stroke-linecap="round"><polyline points="20 6 9 17 4 12"/></svg>Unsubscribe anytime, one click</div>
</div>
</div>
</div>
</div>
<script>
const form = document.getElementById('form');
const emailInput = document.getElementById('emailInput');
const inputWrap = document.getElementById('inputWrap');
const errorMsg = document.getElementById('errorMsg');
const submitBtn = document.getElementById('submitBtn');
const stateIdle = document.getElementById('stateIdle');
const stateSuccess = document.getElementById('stateSuccess');
const confirmedEmail = document.getElementById('confirmedEmail');
function validateEmail(v){ return /^[^s@]+@[^s@]+.[^s@]+$/.test(v.trim()); }
function showError(msg){
errorMsg.textContent = msg;
emailInput.classList.add('error-state');
}
function clearError(){
errorMsg.textContent = '';
emailInput.classList.remove('error-state');
}
emailInput.addEventListener('input', clearError);
form.addEventListener('submit', async e => {
e.preventDefault();
clearError();
const val = emailInput.value.trim();
if(!val){ showError('Please enter your email address.'); emailInput.focus(); return; }
if(!validateEmail(val)){ showError('Please enter a valid email address.'); emailInput.focus(); return; }
submitBtn.classList.add('loading');
submitBtn.disabled = true;
// Simulate API call
await new Promise(r => setTimeout(r, 1400));
confirmedEmail.textContent = val;
stateIdle.hidden = true;
stateSuccess.hidden = false;
stateSuccess.removeAttribute('hidden');
});
</script>
</body>
</html><!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Newsletter Signup Card</title>
<!-- Tailwind CSS v3+ — arbitrary value classes (e.g. bg-[#0f172a]) are valid -->
<script src="https://cdn.tailwindcss.com"></script>
<style>
@keyframes spin{to{transform:rotate(360deg)}}
@keyframes fadeIn{from{opacity:0;transform:translateY(12px)}to{opacity:1;transform:none}}
@keyframes drawCircle{to{stroke-dashoffset:0}}
@keyframes drawCheck{to{stroke-dashoffset:0}}
* {
box-sizing:border-box;margin:0;padding:0
}
body {
font-family:system-ui,sans-serif;background:linear-gradient(135deg,#ede9fe 0%,#e0f2fe 100%);min-height:100vh;display:flex;align-items:center;justify-content:center;padding:20px
}
.input {
width:100%;padding:13px 16px 13px 38px;font-size:14px;border:2px solid #e2e8f0;border-radius:12px;outline:none;color:#1e293b;background:#f8fafc;transition:border-color .2s,background .2s;font-family:inherit
}
.input:focus {
border-color:#6366f1;background:#fff
}
.input.error-state {
border-color:#ef4444
}
.btn.loading .btn-text,.btn.loading .btn-arrow {
opacity:0
}
.btn.loading .btn-spinner {
opacity:1
}
.social-count strong {
color:#475569
}
</style>
</head>
<body>
<div class="page">
<div class="bg-[#fff] rounded-3xl py-9 px-8 max-w-[420px] w-full shadow-[0_20px_60px_rgba(99,102,241,.12)] text-center" id="card">
<div class="state-idle" id="stateIdle">
<div class="w-[52px] h-[52px] [background:linear-gradient(135deg,#ede9fe,#ddd6fe)] rounded-[14px] flex items-center justify-center mt-0 mx-auto mb-5 text-[#7c3aed]">
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/><polyline points="22,6 12,13 2,6"/></svg>
</div>
<h2 class="text-[22px] font-extrabold text-[#1e293b] mb-2">Stay in the loop</h2>
<p class="text-sm text-[#64748b] leading-[1.6] mb-4">Get weekly dev tips, UI snippets, and early access to new tools — no spam, ever.</p>
<div class="flex gap-2 justify-center flex-wrap mb-6">
<span class="text-[11px] font-semibold text-[#6366f1] bg-[#eef2ff] rounded-[20px] py-1 px-2.5">📦 175+ snippets</span>
<span class="text-[11px] font-semibold text-[#6366f1] bg-[#eef2ff] rounded-[20px] py-1 px-2.5">⚡ Weekly tips</span>
<span class="text-[11px] font-semibold text-[#6366f1] bg-[#eef2ff] rounded-[20px] py-1 px-2.5">🔒 No spam</span>
</div>
<form class="form" id="form" novalidate>
<div class="relative flex items-center" id="inputWrap">
<svg class="absolute left-[13px] text-[#94a3b8] pointer-events-none shrink-0" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/><polyline points="22,6 12,13 2,6"/></svg>
<input id="emailInput" type="email" class="input" placeholder="[email protected]" autocomplete="email" aria-label="Email address" required />
</div>
<p class="text-xs text-[#ef4444] mt-1.5 min-h-[16px] text-left" id="errorMsg" role="alert" aria-live="assertive"></p>
<button type="submit" class="btn flex items-center justify-center gap-2 w-full mt-2.5 p-3.5 [background:linear-gradient(135deg,#6366f1,#8b5cf6)] text-[#fff] text-sm font-bold border-0 rounded-xl cursor-pointer [transition:opacity_.2s,transform_.15s] relative overflow-hidden font-[inherit] hover:opacity-[.92] hover:[transform:translateY(-1px)] active:[transform:translateY(0)]" id="submitBtn">
<span class="btn-text">Subscribe</span>
<svg class="btn-arrow" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><path d="M5 12h14"/><path d="m13 6 6 6-6 6"/></svg>
<span class="btn-spinner absolute w-[18px] h-[18px] border [border-top-color:#fff] rounded-full [animation:spin_.7s_linear_infinite] opacity-0 [transition:opacity_.2s]" aria-hidden="true"></span>
</button>
</form>
<p class="social-count text-xs text-[#94a3b8] mt-3.5"><strong>12,847</strong> developers already subscribed</p>
</div>
<div class="[animation:fadeIn_.5s_ease]" id="stateSuccess" hidden>
<div class="w-20 h-20 mt-0 mx-auto mb-5 text-[#10b981]">
<svg class="success-check" viewBox="0 0 52 52" fill="none">
<circle class="[stroke-dasharray:151] [stroke-dashoffset:151] [animation:drawCircle_.6s_.1s_ease_forwards]" cx="26" cy="26" r="24" stroke="currentColor" stroke-width="2.5"/>
<path class="[stroke-dasharray:40] [stroke-dashoffset:40] [animation:drawCheck_.4s_.65s_ease_forwards]" d="M14 26 L22 34 L38 18" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</div>
<h2 class="text-[22px] font-extrabold text-[#1e293b] mb-2">You're subscribed!</h2>
<p class="text-sm text-[#64748b] leading-[1.6] mb-6">Check <strong id="confirmedEmail">your inbox</strong> for a confirmation. Welcome aboard! 🎉</p>
<div class="flex flex-col gap-2.5 text-left bg-[#f0fdf4] border border-[#bbf7d0] rounded-xl p-4">
<div class="flex items-center gap-2 text-[13px] text-[#166534] font-medium"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#10b981" stroke-width="2.5" stroke-linecap="round"><polyline points="20 6 9 17 4 12"/></svg>Weekly dev tips delivered</div>
<div class="flex items-center gap-2 text-[13px] text-[#166534] font-medium"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#10b981" stroke-width="2.5" stroke-linecap="round"><polyline points="20 6 9 17 4 12"/></svg>Early access to new UI snippets</div>
<div class="flex items-center gap-2 text-[13px] text-[#166534] font-medium"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#10b981" stroke-width="2.5" stroke-linecap="round"><polyline points="20 6 9 17 4 12"/></svg>Unsubscribe anytime, one click</div>
</div>
</div>
</div>
</div>
<script>
const form = document.getElementById('form');
const emailInput = document.getElementById('emailInput');
const inputWrap = document.getElementById('inputWrap');
const errorMsg = document.getElementById('errorMsg');
const submitBtn = document.getElementById('submitBtn');
const stateIdle = document.getElementById('stateIdle');
const stateSuccess = document.getElementById('stateSuccess');
const confirmedEmail = document.getElementById('confirmedEmail');
function validateEmail(v){ return /^[^s@]+@[^s@]+.[^s@]+$/.test(v.trim()); }
function showError(msg){
errorMsg.textContent = msg;
emailInput.classList.add('error-state');
}
function clearError(){
errorMsg.textContent = '';
emailInput.classList.remove('error-state');
}
emailInput.addEventListener('input', clearError);
form.addEventListener('submit', async e => {
e.preventDefault();
clearError();
const val = emailInput.value.trim();
if(!val){ showError('Please enter your email address.'); emailInput.focus(); return; }
if(!validateEmail(val)){ showError('Please enter a valid email address.'); emailInput.focus(); return; }
submitBtn.classList.add('loading');
submitBtn.disabled = true;
// Simulate API call
await new Promise(r => setTimeout(r, 1400));
confirmedEmail.textContent = val;
stateIdle.hidden = true;
stateSuccess.hidden = false;
stateSuccess.removeAttribute('hidden');
});
</script>
</body>
</html>import React, { useEffect } from 'react';
// CSS — optionally move to NewsletterSignupCard.module.css
const css = `
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,sans-serif;background:linear-gradient(135deg,#ede9fe 0%,#e0f2fe 100%);min-height:100vh;display:flex;align-items:center;justify-content:center;padding:20px}
.card{background:#fff;border-radius:24px;padding:36px 32px;max-width:420px;width:100%;box-shadow:0 20px 60px rgba(99,102,241,.12);text-align:center}
/* Idle state */
.icon-wrap{width:52px;height:52px;background:linear-gradient(135deg,#ede9fe,#ddd6fe);border-radius:14px;display:flex;align-items:center;justify-content:center;margin:0 auto 20px;color:#7c3aed}
.title{font-size:22px;font-weight:800;color:#1e293b;margin-bottom:8px}
.desc{font-size:14px;color:#64748b;line-height:1.6;margin-bottom:16px}
.badges{display:flex;gap:8px;justify-content:center;flex-wrap:wrap;margin-bottom:24px}
.badge{font-size:11px;font-weight:600;color:#6366f1;background:#eef2ff;border-radius:20px;padding:4px 10px}
.input-wrap{position:relative;display:flex;align-items:center}
.input-icon{position:absolute;left:13px;color:#94a3b8;pointer-events:none;flex-shrink:0}
.input{width:100%;padding:13px 16px 13px 38px;font-size:14px;border:2px solid #e2e8f0;border-radius:12px;outline:none;color:#1e293b;background:#f8fafc;transition:border-color .2s,background .2s;font-family:inherit}
.input:focus{border-color:#6366f1;background:#fff}
.input.error-state{border-color:#ef4444}
.error-msg{font-size:12px;color:#ef4444;margin-top:6px;min-height:16px;text-align:left}
.btn{display:flex;align-items:center;justify-content:center;gap:8px;width:100%;margin-top:10px;padding:14px;background:linear-gradient(135deg,#6366f1,#8b5cf6);color:#fff;font-size:14px;font-weight:700;border:none;border-radius:12px;cursor:pointer;transition:opacity .2s,transform .15s;position:relative;overflow:hidden;font-family:inherit}
.btn:hover{opacity:.92;transform:translateY(-1px)}
.btn:active{transform:translateY(0)}
.btn.loading .btn-text,.btn.loading .btn-arrow{opacity:0}
.btn.loading .btn-spinner{opacity:1}
.btn-spinner{position:absolute;width:18px;height:18px;border:2.5px solid rgba(255,255,255,.3);border-top-color:#fff;border-radius:50%;animation:spin .7s linear infinite;opacity:0;transition:opacity .2s}
@keyframes spin{to{transform:rotate(360deg)}}
.social-count{font-size:12px;color:#94a3b8;margin-top:14px}
.social-count strong{color:#475569}
/* Success state */
.state-success{animation:fadeIn .5s ease}
@keyframes fadeIn{from{opacity:0;transform:translateY(12px)}to{opacity:1;transform:none}}
.success-ring{width:80px;height:80px;margin:0 auto 20px;color:#10b981}
.check-circle{stroke-dasharray:151;stroke-dashoffset:151;animation:drawCircle .6s .1s ease forwards}
.check-path{stroke-dasharray:40;stroke-dashoffset:40;animation:drawCheck .4s .65s ease forwards}
@keyframes drawCircle{to{stroke-dashoffset:0}}
@keyframes drawCheck{to{stroke-dashoffset:0}}
.success-title{font-size:22px;font-weight:800;color:#1e293b;margin-bottom:8px}
.success-desc{font-size:14px;color:#64748b;line-height:1.6;margin-bottom:24px}
.success-perks{display:flex;flex-direction:column;gap:10px;text-align:left;background:#f0fdf4;border:1px solid #bbf7d0;border-radius:12px;padding:16px}
.perk{display:flex;align-items:center;gap:8px;font-size:13px;color:#166534;font-weight:500}
`;
export default function NewsletterSignupCard() {
// Auto-generated escape hatch: the original snippet's vanilla JS runs once
// after mount and queries the rendered DOM. For idiomatic React, lift this
// into state + handlers.
useEffect(() => {
const _listeners = [];
const _originalAddEventListener = EventTarget.prototype.addEventListener;
EventTarget.prototype.addEventListener = function(type, listener, options) {
_listeners.push({ target: this, type, listener, options });
return _originalAddEventListener.call(this, type, listener, options);
};
const form = document.getElementById('form');
const emailInput = document.getElementById('emailInput');
const inputWrap = document.getElementById('inputWrap');
const errorMsg = document.getElementById('errorMsg');
const submitBtn = document.getElementById('submitBtn');
const stateIdle = document.getElementById('stateIdle');
const stateSuccess = document.getElementById('stateSuccess');
const confirmedEmail = document.getElementById('confirmedEmail');
function validateEmail(v){ return /^[^s@]+@[^s@]+.[^s@]+$/.test(v.trim()); }
function showError(msg){
errorMsg.textContent = msg;
emailInput.classList.add('error-state');
}
function clearError(){
errorMsg.textContent = '';
emailInput.classList.remove('error-state');
}
emailInput.addEventListener('input', clearError);
form.addEventListener('submit', async e => {
e.preventDefault();
clearError();
const val = emailInput.value.trim();
if(!val){ showError('Please enter your email address.'); emailInput.focus(); return; }
if(!validateEmail(val)){ showError('Please enter a valid email address.'); emailInput.focus(); return; }
submitBtn.classList.add('loading');
submitBtn.disabled = true;
// Simulate API call
await new Promise(r => setTimeout(r, 1400));
confirmedEmail.textContent = val;
stateIdle.hidden = true;
stateSuccess.hidden = false;
stateSuccess.removeAttribute('hidden');
});
// Cleanup: restore addEventListener and remove all listeners
return () => {
EventTarget.prototype.addEventListener = _originalAddEventListener;
for (const { target, type, listener, options } of _listeners) {
target.removeEventListener(type, listener, options);
}
};
}, []);
return (
<>
<style>{css}</style>
<div className="page">
<div className="card" id="card">
<div className="state-idle" id="stateIdle">
<div className="icon-wrap">
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" stroke-linejoin="round"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/><polyline points="22,6 12,13 2,6"/></svg>
</div>
<h2 className="title">Stay in the loop</h2>
<p className="desc">Get weekly dev tips, UI snippets, and early access to new tools — no spam, ever.</p>
<div className="badges">
<span className="badge">📦 175+ snippets</span>
<span className="badge">⚡ Weekly tips</span>
<span className="badge">🔒 No spam</span>
</div>
<form className="form" id="form" novalidate>
<div className="input-wrap" id="inputWrap">
<svg className="input-icon" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/><polyline points="22,6 12,13 2,6"/></svg>
<input id="emailInput" type="email" className="input" placeholder="[email protected]" autocomplete="email" aria-label="Email address" required />
</div>
<p className="error-msg" id="errorMsg" role="alert" aria-live="assertive"></p>
<button type="submit" className="btn" id="submitBtn">
<span className="btn-text">Subscribe</span>
<svg className="btn-arrow" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round"><path d="M5 12h14"/><path d="m13 6 6 6-6 6"/></svg>
<span className="btn-spinner" aria-hidden="true"></span>
</button>
</form>
<p className="social-count"><strong>12,847</strong> developers already subscribed</p>
</div>
<div className="state-success" id="stateSuccess" hidden>
<div className="success-ring">
<svg className="success-check" viewBox="0 0 52 52" fill="none">
<circle className="check-circle" cx="26" cy="26" r="24" stroke="currentColor" strokeWidth="2.5"/>
<path className="check-path" d="M14 26 L22 34 L38 18" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" stroke-linejoin="round"/>
</svg>
</div>
<h2 className="success-title">You're subscribed!</h2>
<p className="success-desc">Check <strong id="confirmedEmail">your inbox</strong> for a confirmation. Welcome aboard! 🎉</p>
<div className="success-perks">
<div className="perk"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#10b981" strokeWidth="2.5" strokeLinecap="round"><polyline points="20 6 9 17 4 12"/></svg>Weekly dev tips delivered</div>
<div className="perk"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#10b981" strokeWidth="2.5" strokeLinecap="round"><polyline points="20 6 9 17 4 12"/></svg>Early access to new UI snippets</div>
<div className="perk"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#10b981" strokeWidth="2.5" strokeLinecap="round"><polyline points="20 6 9 17 4 12"/></svg>Unsubscribe anytime, one click</div>
</div>
</div>
</div>
</div>
</>
);
}import React, { useEffect } from 'react';
// Requires Tailwind CSS v3+ — https://tailwindcss.com/docs/installation
// Arbitrary value classes (e.g. bg-[#0f172a]) are valid Tailwind v3+
export default function NewsletterSignupCard() {
// Auto-generated escape hatch: the original snippet's vanilla JS runs once
// after mount and queries the rendered DOM. For idiomatic React, lift this
// into state + handlers.
useEffect(() => {
const _listeners = [];
const _originalAddEventListener = EventTarget.prototype.addEventListener;
EventTarget.prototype.addEventListener = function(type, listener, options) {
_listeners.push({ target: this, type, listener, options });
return _originalAddEventListener.call(this, type, listener, options);
};
const form = document.getElementById('form');
const emailInput = document.getElementById('emailInput');
const inputWrap = document.getElementById('inputWrap');
const errorMsg = document.getElementById('errorMsg');
const submitBtn = document.getElementById('submitBtn');
const stateIdle = document.getElementById('stateIdle');
const stateSuccess = document.getElementById('stateSuccess');
const confirmedEmail = document.getElementById('confirmedEmail');
function validateEmail(v){ return /^[^s@]+@[^s@]+.[^s@]+$/.test(v.trim()); }
function showError(msg){
errorMsg.textContent = msg;
emailInput.classList.add('error-state');
}
function clearError(){
errorMsg.textContent = '';
emailInput.classList.remove('error-state');
}
emailInput.addEventListener('input', clearError);
form.addEventListener('submit', async e => {
e.preventDefault();
clearError();
const val = emailInput.value.trim();
if(!val){ showError('Please enter your email address.'); emailInput.focus(); return; }
if(!validateEmail(val)){ showError('Please enter a valid email address.'); emailInput.focus(); return; }
submitBtn.classList.add('loading');
submitBtn.disabled = true;
// Simulate API call
await new Promise(r => setTimeout(r, 1400));
confirmedEmail.textContent = val;
stateIdle.hidden = true;
stateSuccess.hidden = false;
stateSuccess.removeAttribute('hidden');
});
// Cleanup: restore addEventListener and remove all listeners
return () => {
EventTarget.prototype.addEventListener = _originalAddEventListener;
for (const { target, type, listener, options } of _listeners) {
target.removeEventListener(type, listener, options);
}
};
}, []);
return (
<>
<style>{`
@keyframes spin{to{transform:rotate(360deg)}}
@keyframes fadeIn{from{opacity:0;transform:translateY(12px)}to{opacity:1;transform:none}}
@keyframes drawCircle{to{stroke-dashoffset:0}}
@keyframes drawCheck{to{stroke-dashoffset:0}}
* {
box-sizing:border-box;margin:0;padding:0
}
body {
font-family:system-ui,sans-serif;background:linear-gradient(135deg,#ede9fe 0%,#e0f2fe 100%);min-height:100vh;display:flex;align-items:center;justify-content:center;padding:20px
}
.input {
width:100%;padding:13px 16px 13px 38px;font-size:14px;border:2px solid #e2e8f0;border-radius:12px;outline:none;color:#1e293b;background:#f8fafc;transition:border-color .2s,background .2s;font-family:inherit
}
.input:focus {
border-color:#6366f1;background:#fff
}
.input.error-state {
border-color:#ef4444
}
.btn.loading .btn-text,.btn.loading .btn-arrow {
opacity:0
}
.btn.loading .btn-spinner {
opacity:1
}
.social-count strong {
color:#475569
}
`}</style>
<div className="page">
<div className="bg-[#fff] rounded-3xl py-9 px-8 max-w-[420px] w-full shadow-[0_20px_60px_rgba(99,102,241,.12)] text-center" id="card">
<div className="state-idle" id="stateIdle">
<div className="w-[52px] h-[52px] [background:linear-gradient(135deg,#ede9fe,#ddd6fe)] rounded-[14px] flex items-center justify-center mt-0 mx-auto mb-5 text-[#7c3aed]">
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" stroke-linejoin="round"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/><polyline points="22,6 12,13 2,6"/></svg>
</div>
<h2 className="text-[22px] font-extrabold text-[#1e293b] mb-2">Stay in the loop</h2>
<p className="text-sm text-[#64748b] leading-[1.6] mb-4">Get weekly dev tips, UI snippets, and early access to new tools — no spam, ever.</p>
<div className="flex gap-2 justify-center flex-wrap mb-6">
<span className="text-[11px] font-semibold text-[#6366f1] bg-[#eef2ff] rounded-[20px] py-1 px-2.5">📦 175+ snippets</span>
<span className="text-[11px] font-semibold text-[#6366f1] bg-[#eef2ff] rounded-[20px] py-1 px-2.5">⚡ Weekly tips</span>
<span className="text-[11px] font-semibold text-[#6366f1] bg-[#eef2ff] rounded-[20px] py-1 px-2.5">🔒 No spam</span>
</div>
<form className="form" id="form" novalidate>
<div className="relative flex items-center" id="inputWrap">
<svg className="absolute left-[13px] text-[#94a3b8] pointer-events-none shrink-0" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/><polyline points="22,6 12,13 2,6"/></svg>
<input id="emailInput" type="email" className="input" placeholder="[email protected]" autocomplete="email" aria-label="Email address" required />
</div>
<p className="text-xs text-[#ef4444] mt-1.5 min-h-[16px] text-left" id="errorMsg" role="alert" aria-live="assertive"></p>
<button type="submit" className="btn flex items-center justify-center gap-2 w-full mt-2.5 p-3.5 [background:linear-gradient(135deg,#6366f1,#8b5cf6)] text-[#fff] text-sm font-bold border-0 rounded-xl cursor-pointer [transition:opacity_.2s,transform_.15s] relative overflow-hidden font-[inherit] hover:opacity-[.92] hover:[transform:translateY(-1px)] active:[transform:translateY(0)]" id="submitBtn">
<span className="btn-text">Subscribe</span>
<svg className="btn-arrow" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round"><path d="M5 12h14"/><path d="m13 6 6 6-6 6"/></svg>
<span className="btn-spinner absolute w-[18px] h-[18px] border [border-top-color:#fff] rounded-full [animation:spin_.7s_linear_infinite] opacity-0 [transition:opacity_.2s]" aria-hidden="true"></span>
</button>
</form>
<p className="social-count text-xs text-[#94a3b8] mt-3.5"><strong>12,847</strong> developers already subscribed</p>
</div>
<div className="[animation:fadeIn_.5s_ease]" id="stateSuccess" hidden>
<div className="w-20 h-20 mt-0 mx-auto mb-5 text-[#10b981]">
<svg className="success-check" viewBox="0 0 52 52" fill="none">
<circle className="[stroke-dasharray:151] [stroke-dashoffset:151] [animation:drawCircle_.6s_.1s_ease_forwards]" cx="26" cy="26" r="24" stroke="currentColor" strokeWidth="2.5"/>
<path className="[stroke-dasharray:40] [stroke-dashoffset:40] [animation:drawCheck_.4s_.65s_ease_forwards]" d="M14 26 L22 34 L38 18" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" stroke-linejoin="round"/>
</svg>
</div>
<h2 className="text-[22px] font-extrabold text-[#1e293b] mb-2">You're subscribed!</h2>
<p className="text-sm text-[#64748b] leading-[1.6] mb-6">Check <strong id="confirmedEmail">your inbox</strong> for a confirmation. Welcome aboard! 🎉</p>
<div className="flex flex-col gap-2.5 text-left bg-[#f0fdf4] border border-[#bbf7d0] rounded-xl p-4">
<div className="flex items-center gap-2 text-[13px] text-[#166534] font-medium"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#10b981" strokeWidth="2.5" strokeLinecap="round"><polyline points="20 6 9 17 4 12"/></svg>Weekly dev tips delivered</div>
<div className="flex items-center gap-2 text-[13px] text-[#166534] font-medium"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#10b981" strokeWidth="2.5" strokeLinecap="round"><polyline points="20 6 9 17 4 12"/></svg>Early access to new UI snippets</div>
<div className="flex items-center gap-2 text-[13px] text-[#166534] font-medium"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#10b981" strokeWidth="2.5" strokeLinecap="round"><polyline points="20 6 9 17 4 12"/></svg>Unsubscribe anytime, one click</div>
</div>
</div>
</div>
</div>
</>
);
}<template>
<div class="page">
<div class="card" id="card">
<div class="state-idle" id="stateIdle">
<div class="icon-wrap">
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/><polyline points="22,6 12,13 2,6"/></svg>
</div>
<h2 class="title">Stay in the loop</h2>
<p class="desc">Get weekly dev tips, UI snippets, and early access to new tools — no spam, ever.</p>
<div class="badges">
<span class="badge">📦 175+ snippets</span>
<span class="badge">⚡ Weekly tips</span>
<span class="badge">🔒 No spam</span>
</div>
<form class="form" id="form" novalidate>
<div class="input-wrap" id="inputWrap">
<svg class="input-icon" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/><polyline points="22,6 12,13 2,6"/></svg>
<input id="emailInput" type="email" class="input" placeholder="[email protected]" autocomplete="email" aria-label="Email address" required />
</div>
<p class="error-msg" id="errorMsg" role="alert" aria-live="assertive"></p>
<button type="submit" class="btn" id="submitBtn">
<span class="btn-text">Subscribe</span>
<svg class="btn-arrow" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><path d="M5 12h14"/><path d="m13 6 6 6-6 6"/></svg>
<span class="btn-spinner" aria-hidden="true"></span>
</button>
</form>
<p class="social-count"><strong>12,847</strong> developers already subscribed</p>
</div>
<div class="state-success" id="stateSuccess" hidden>
<div class="success-ring">
<svg class="success-check" viewBox="0 0 52 52" fill="none">
<circle class="check-circle" cx="26" cy="26" r="24" stroke="currentColor" stroke-width="2.5"/>
<path class="check-path" d="M14 26 L22 34 L38 18" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</div>
<h2 class="success-title">You're subscribed!</h2>
<p class="success-desc">Check <strong id="confirmedEmail">your inbox</strong> for a confirmation. Welcome aboard! 🎉</p>
<div class="success-perks">
<div class="perk"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#10b981" stroke-width="2.5" stroke-linecap="round"><polyline points="20 6 9 17 4 12"/></svg>Weekly dev tips delivered</div>
<div class="perk"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#10b981" stroke-width="2.5" stroke-linecap="round"><polyline points="20 6 9 17 4 12"/></svg>Early access to new UI snippets</div>
<div class="perk"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#10b981" stroke-width="2.5" stroke-linecap="round"><polyline points="20 6 9 17 4 12"/></svg>Unsubscribe anytime, one click</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { onMounted } from 'vue';
let form;
let emailInput;
let inputWrap;
let errorMsg;
let submitBtn;
let stateIdle;
let stateSuccess;
let confirmedEmail;
function validateEmail(v){ return /^[^s@]+@[^s@]+.[^s@]+$/.test(v.trim()); }
function showError(msg){
errorMsg.textContent = msg;
emailInput.classList.add('error-state');
}
function clearError(){
errorMsg.textContent = '';
emailInput.classList.remove('error-state');
}
onMounted(() => {
form = document.getElementById('form');
emailInput = document.getElementById('emailInput');
inputWrap = document.getElementById('inputWrap');
errorMsg = document.getElementById('errorMsg');
submitBtn = document.getElementById('submitBtn');
stateIdle = document.getElementById('stateIdle');
stateSuccess = document.getElementById('stateSuccess');
confirmedEmail = document.getElementById('confirmedEmail');
emailInput.addEventListener('input', clearError);
form.addEventListener('submit', async e => {
e.preventDefault();
clearError();
const val = emailInput.value.trim();
if(!val){ showError('Please enter your email address.'); emailInput.focus(); return; }
if(!validateEmail(val)){ showError('Please enter a valid email address.'); emailInput.focus(); return; }
submitBtn.classList.add('loading');
submitBtn.disabled = true;
// Simulate API call
await new Promise(r => setTimeout(r, 1400));
confirmedEmail.textContent = val;
stateIdle.hidden = true;
stateSuccess.hidden = false;
stateSuccess.removeAttribute('hidden');
});
});
</script>
<style scoped>
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,sans-serif;background:linear-gradient(135deg,#ede9fe 0%,#e0f2fe 100%);min-height:100vh;display:flex;align-items:center;justify-content:center;padding:20px}
.card{background:#fff;border-radius:24px;padding:36px 32px;max-width:420px;width:100%;box-shadow:0 20px 60px rgba(99,102,241,.12);text-align:center}
/* Idle state */
.icon-wrap{width:52px;height:52px;background:linear-gradient(135deg,#ede9fe,#ddd6fe);border-radius:14px;display:flex;align-items:center;justify-content:center;margin:0 auto 20px;color:#7c3aed}
.title{font-size:22px;font-weight:800;color:#1e293b;margin-bottom:8px}
.desc{font-size:14px;color:#64748b;line-height:1.6;margin-bottom:16px}
.badges{display:flex;gap:8px;justify-content:center;flex-wrap:wrap;margin-bottom:24px}
.badge{font-size:11px;font-weight:600;color:#6366f1;background:#eef2ff;border-radius:20px;padding:4px 10px}
.input-wrap{position:relative;display:flex;align-items:center}
.input-icon{position:absolute;left:13px;color:#94a3b8;pointer-events:none;flex-shrink:0}
.input{width:100%;padding:13px 16px 13px 38px;font-size:14px;border:2px solid #e2e8f0;border-radius:12px;outline:none;color:#1e293b;background:#f8fafc;transition:border-color .2s,background .2s;font-family:inherit}
.input:focus{border-color:#6366f1;background:#fff}
.input.error-state{border-color:#ef4444}
.error-msg{font-size:12px;color:#ef4444;margin-top:6px;min-height:16px;text-align:left}
.btn{display:flex;align-items:center;justify-content:center;gap:8px;width:100%;margin-top:10px;padding:14px;background:linear-gradient(135deg,#6366f1,#8b5cf6);color:#fff;font-size:14px;font-weight:700;border:none;border-radius:12px;cursor:pointer;transition:opacity .2s,transform .15s;position:relative;overflow:hidden;font-family:inherit}
.btn:hover{opacity:.92;transform:translateY(-1px)}
.btn:active{transform:translateY(0)}
.btn.loading .btn-text,.btn.loading .btn-arrow{opacity:0}
.btn.loading .btn-spinner{opacity:1}
.btn-spinner{position:absolute;width:18px;height:18px;border:2.5px solid rgba(255,255,255,.3);border-top-color:#fff;border-radius:50%;animation:spin .7s linear infinite;opacity:0;transition:opacity .2s}
@keyframes spin{to{transform:rotate(360deg)}}
.social-count{font-size:12px;color:#94a3b8;margin-top:14px}
.social-count strong{color:#475569}
/* Success state */
.state-success{animation:fadeIn .5s ease}
@keyframes fadeIn{from{opacity:0;transform:translateY(12px)}to{opacity:1;transform:none}}
.success-ring{width:80px;height:80px;margin:0 auto 20px;color:#10b981}
.check-circle{stroke-dasharray:151;stroke-dashoffset:151;animation:drawCircle .6s .1s ease forwards}
.check-path{stroke-dasharray:40;stroke-dashoffset:40;animation:drawCheck .4s .65s ease forwards}
@keyframes drawCircle{to{stroke-dashoffset:0}}
@keyframes drawCheck{to{stroke-dashoffset:0}}
.success-title{font-size:22px;font-weight:800;color:#1e293b;margin-bottom:8px}
.success-desc{font-size:14px;color:#64748b;line-height:1.6;margin-bottom:24px}
.success-perks{display:flex;flex-direction:column;gap:10px;text-align:left;background:#f0fdf4;border:1px solid #bbf7d0;border-radius:12px;padding:16px}
.perk{display:flex;align-items:center;gap:8px;font-size:13px;color:#166534;font-weight:500}
</style>// @ts-nocheck
// Note: vanilla JS DOM manipulation is preserved as-is inside ngAfterViewInit().
// For idiomatic Angular, replace document.getElementById() with @ViewChild() refs
// and move state into component properties with two-way binding.
import { Component, AfterViewInit, ViewEncapsulation } from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-newsletter-signup-card',
standalone: true,
imports: [CommonModule],
encapsulation: ViewEncapsulation.None,
template: `
<div class="page">
<div class="card" id="card">
<div class="state-idle" id="stateIdle">
<div class="icon-wrap">
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/><polyline points="22,6 12,13 2,6"/></svg>
</div>
<h2 class="title">Stay in the loop</h2>
<p class="desc">Get weekly dev tips, UI snippets, and early access to new tools — no spam, ever.</p>
<div class="badges">
<span class="badge">📦 175+ snippets</span>
<span class="badge">⚡ Weekly tips</span>
<span class="badge">🔒 No spam</span>
</div>
<form class="form" id="form" novalidate>
<div class="input-wrap" id="inputWrap">
<svg class="input-icon" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/><polyline points="22,6 12,13 2,6"/></svg>
<input id="emailInput" type="email" class="input" placeholder="[email protected]" autocomplete="email" aria-label="Email address" required />
</div>
<p class="error-msg" id="errorMsg" role="alert" aria-live="assertive"></p>
<button type="submit" class="btn" id="submitBtn">
<span class="btn-text">Subscribe</span>
<svg class="btn-arrow" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><path d="M5 12h14"/><path d="m13 6 6 6-6 6"/></svg>
<span class="btn-spinner" aria-hidden="true"></span>
</button>
</form>
<p class="social-count"><strong>12,847</strong> developers already subscribed</p>
</div>
<div class="state-success" id="stateSuccess" hidden>
<div class="success-ring">
<svg class="success-check" viewBox="0 0 52 52" fill="none">
<circle class="check-circle" cx="26" cy="26" r="24" stroke="currentColor" stroke-width="2.5"/>
<path class="check-path" d="M14 26 L22 34 L38 18" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</div>
<h2 class="success-title">You're subscribed!</h2>
<p class="success-desc">Check <strong id="confirmedEmail">your inbox</strong> for a confirmation. Welcome aboard! 🎉</p>
<div class="success-perks">
<div class="perk"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#10b981" stroke-width="2.5" stroke-linecap="round"><polyline points="20 6 9 17 4 12"/></svg>Weekly dev tips delivered</div>
<div class="perk"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#10b981" stroke-width="2.5" stroke-linecap="round"><polyline points="20 6 9 17 4 12"/></svg>Early access to new UI snippets</div>
<div class="perk"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#10b981" stroke-width="2.5" stroke-linecap="round"><polyline points="20 6 9 17 4 12"/></svg>Unsubscribe anytime, one click</div>
</div>
</div>
</div>
</div>
`,
styles: [`
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,sans-serif;background:linear-gradient(135deg,#ede9fe 0%,#e0f2fe 100%);min-height:100vh;display:flex;align-items:center;justify-content:center;padding:20px}
.card{background:#fff;border-radius:24px;padding:36px 32px;max-width:420px;width:100%;box-shadow:0 20px 60px rgba(99,102,241,.12);text-align:center}
/* Idle state */
.icon-wrap{width:52px;height:52px;background:linear-gradient(135deg,#ede9fe,#ddd6fe);border-radius:14px;display:flex;align-items:center;justify-content:center;margin:0 auto 20px;color:#7c3aed}
.title{font-size:22px;font-weight:800;color:#1e293b;margin-bottom:8px}
.desc{font-size:14px;color:#64748b;line-height:1.6;margin-bottom:16px}
.badges{display:flex;gap:8px;justify-content:center;flex-wrap:wrap;margin-bottom:24px}
.badge{font-size:11px;font-weight:600;color:#6366f1;background:#eef2ff;border-radius:20px;padding:4px 10px}
.input-wrap{position:relative;display:flex;align-items:center}
.input-icon{position:absolute;left:13px;color:#94a3b8;pointer-events:none;flex-shrink:0}
.input{width:100%;padding:13px 16px 13px 38px;font-size:14px;border:2px solid #e2e8f0;border-radius:12px;outline:none;color:#1e293b;background:#f8fafc;transition:border-color .2s,background .2s;font-family:inherit}
.input:focus{border-color:#6366f1;background:#fff}
.input.error-state{border-color:#ef4444}
.error-msg{font-size:12px;color:#ef4444;margin-top:6px;min-height:16px;text-align:left}
.btn{display:flex;align-items:center;justify-content:center;gap:8px;width:100%;margin-top:10px;padding:14px;background:linear-gradient(135deg,#6366f1,#8b5cf6);color:#fff;font-size:14px;font-weight:700;border:none;border-radius:12px;cursor:pointer;transition:opacity .2s,transform .15s;position:relative;overflow:hidden;font-family:inherit}
.btn:hover{opacity:.92;transform:translateY(-1px)}
.btn:active{transform:translateY(0)}
.btn.loading .btn-text,.btn.loading .btn-arrow{opacity:0}
.btn.loading .btn-spinner{opacity:1}
.btn-spinner{position:absolute;width:18px;height:18px;border:2.5px solid rgba(255,255,255,.3);border-top-color:#fff;border-radius:50%;animation:spin .7s linear infinite;opacity:0;transition:opacity .2s}
@keyframes spin{to{transform:rotate(360deg)}}
.social-count{font-size:12px;color:#94a3b8;margin-top:14px}
.social-count strong{color:#475569}
/* Success state */
.state-success{animation:fadeIn .5s ease}
@keyframes fadeIn{from{opacity:0;transform:translateY(12px)}to{opacity:1;transform:none}}
.success-ring{width:80px;height:80px;margin:0 auto 20px;color:#10b981}
.check-circle{stroke-dasharray:151;stroke-dashoffset:151;animation:drawCircle .6s .1s ease forwards}
.check-path{stroke-dasharray:40;stroke-dashoffset:40;animation:drawCheck .4s .65s ease forwards}
@keyframes drawCircle{to{stroke-dashoffset:0}}
@keyframes drawCheck{to{stroke-dashoffset:0}}
.success-title{font-size:22px;font-weight:800;color:#1e293b;margin-bottom:8px}
.success-desc{font-size:14px;color:#64748b;line-height:1.6;margin-bottom:24px}
.success-perks{display:flex;flex-direction:column;gap:10px;text-align:left;background:#f0fdf4;border:1px solid #bbf7d0;border-radius:12px;padding:16px}
.perk{display:flex;align-items:center;gap:8px;font-size:13px;color:#166534;font-weight:500}
`]
})
export class NewsletterSignupCardComponent implements AfterViewInit {
ngAfterViewInit(): void {
const form = document.getElementById('form');
const emailInput = document.getElementById('emailInput');
const inputWrap = document.getElementById('inputWrap');
const errorMsg = document.getElementById('errorMsg');
const submitBtn = document.getElementById('submitBtn');
const stateIdle = document.getElementById('stateIdle');
const stateSuccess = document.getElementById('stateSuccess');
const confirmedEmail = document.getElementById('confirmedEmail');
function validateEmail(v){ return /^[^s@]+@[^s@]+.[^s@]+$/.test(v.trim()); }
function showError(msg){
errorMsg.textContent = msg;
emailInput.classList.add('error-state');
}
function clearError(){
errorMsg.textContent = '';
emailInput.classList.remove('error-state');
}
emailInput.addEventListener('input', clearError);
form.addEventListener('submit', async e => {
e.preventDefault();
clearError();
const val = emailInput.value.trim();
if(!val){ showError('Please enter your email address.'); emailInput.focus(); return; }
if(!validateEmail(val)){ showError('Please enter a valid email address.'); emailInput.focus(); return; }
submitBtn.classList.add('loading');
submitBtn.disabled = true;
// Simulate API call
await new Promise(r => setTimeout(r, 1400));
confirmedEmail.textContent = val;
stateIdle.hidden = true;
stateSuccess.hidden = false;
stateSuccess.removeAttribute('hidden');
});
// Bind inline-handler functions to the component so the template can call them
Object.assign(this, { validateEmail, showError, clearError });
}
}