Newsletter Signup Card — Animated Success State HTML CSS JS

Newsletter Signup Card · Forms · Plain HTML, CSS & JS · Live preview

Share & Support

What's included

Features

Client-side email validation
/^[^s@]+@[^s@]+.[^s@]+$/ catches obvious format errors. role="alert" + aria-live="assertive" announces errors to screen readers immediately.
Loading button with spinner
.loading class fades out button text and shows a centred CSS spinner, keeping button dimensions fixed to prevent layout shift during the async delay.
Animated SVG checkmark
Circle and check path use stroke-dashoffset keyframe animations that draw in sequence — circle first (0.6s), then checkmark (0.4s, delayed 0.65s).
Two-state card (idle / success)
Both states live in the DOM with the hidden attribute toggled on submit. The success state uses animation: fadeIn to slide up, avoiding layout jump.
Inline error + red border
Errors display in a <p role="alert"> below the input and add .error-state class (red border). The input listener clears errors on every keystroke — immediate feedback.
Email echo in success state
The confirmed email is injected into the success message (confirmedEmail.textContent = val) — a small but high-trust detail that confirms the correct address was saved.
Social proof counter
"12,847 developers already subscribed" below the button uses established-community social proof — one of the highest-converting newsletter conversion elements.
Feature badge pills
Three emoji badge pills ("📦 175+ snippets", "⚡ Weekly tips", "🔒 No spam") address the three main objections before the user reaches the input.

About this UI Snippet

Newsletter Signup Card — Form Validation, Animated SVG Check & State Machine Pattern

Screenshot of the Newsletter Signup Card snippet rendered live

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

  1. 1
    Paste HTML, CSS, and JSA newsletter card appears with an email icon, headline, feature badges, email input, and subscribe button on a purple gradient background.
  2. 2
    Try 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.
  3. 3
    Try an invalid emailEnter "test" and submit — a validation error appears. Enter "[email protected]" and submit to proceed.
  4. 4
    Watch the loading stateA spinner appears inside the button for ~1.4 seconds (simulating an API call). The button disables to prevent double submission.
  5. 5
    See 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.
  6. 6
    Connect 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 the then handler, or re-show the form with an error message if the call fails.

Real-world uses

Common Use Cases

Blog and content site email lists
The primary use case — capture returning visitors with a weekly newsletter signup. Position it in the footer, sidebar, or as a mid-article insert after the first few paragraphs.
SaaS waitlist and early access
"Be first to know when we launch" with the same two-state form. The success state confirms the user is on the list. Add a referral count for viral waitlist mechanics.
Product update announcements
Subscribe users to product changelogs, release notes, or feature announcements. The perks section can list specific content types to set expectations.
Event and webinar registration
Replace "newsletter" framing with "register for free webinar" and update the success state to include the event date and calendar link.
Developer tool documentation sites
Docs sites for libraries and APIs use newsletter signup to notify users of breaking changes, new versions, and security updates.
E-commerce promotional emails
"Get 10% off your first order" signup with the same form. The success state shows the discount code instead of the perks list.

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

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>
  <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>
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>
  <!-- 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>
React
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>
    </>
  );
}
React + Tailwind
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>
    </>
  );
}
Vue
<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>
Angular
// @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 });
  }
}