Magic Link Login — Passwordless Email Sign-In

Magic Link Login · Forms · Plain HTML, CSS & JS · Live preview

Share & Support

What's included

Features

Two-state card, no page navigation
The email form and check-your-email confirmation share one container, toggled via hidden — no route change needed.
Practical email validation
A pragmatic regex catches the common invalid shapes without claiming full RFC email compliance.
Distinct empty vs. invalid error messages
Users get a specific reason for the error rather than one generic "invalid input" message.
Real resend cooldown
A live 30-second countdown disables the resend button, preventing accidental or abusive repeat sends.
Simulated async submit state
The submit button disables and relabels to "Sending…" during the request, reading as a real network call.
"Use a different email" reset
Going back clears the countdown timer and refocuses the input, avoiding stale background timers.
No password field anywhere
The entire flow is built around the passwordless pattern from the first screen, not a password field with a "forgot password" afterthought.
Clean, focused single-purpose card
No unrelated UI competing for attention — the card does exactly one job, the way a real auth screen should.

About this UI Snippet

Magic Link Login — Passwordless Sign-In, Resend Cooldown & Check-Your-Email State

Screenshot of the Magic Link Login snippet rendered live

Passwordless sign-in removes the single biggest source of authentication friction — a forgotten password — by sending a one-time sign-in link to a verified email instead. This snippet builds the complete client-side flow: email validation, a simulated send, a "check your email" confirmation screen, and a resend button gated by a real countdown so it can't be spammed.

Two screens, one card

The component is really two states sharing one container: the email-entry form and the check-your-email confirmation, toggled via hidden. Rather than navigating to a new page, swapping the inner content keeps the user anchored to the same card, which matters for a flow whose entire job is to get the user comfortable waiting for an email — a page navigation here would feel like the flow had "moved on" without them.

Validation before the simulated send

A simple regex (/^[^\s@]+@[^\s@]+\.[^\s@]+$/) checks for a plausible email shape — not full RFC 5322 compliance, which is famously a rabbit hole no client-side regex fully solves, but the basic [email protected] shape that catches the vast majority of real typos. An empty field and an invalid shape get distinct messages, and the submit button disables with a "Sending…" label during the simulated 900ms delay, so the interaction reads as a real network round trip.

A resend button with teeth

The "Resend link" button isn't just decorative — clicking the original submit (or resend) starts a real 30-second countdown via setInterval, during which the resend button is disabled and shows "Resend link in 27s", ticking down live. This is a meaningful anti-abuse pattern: without it, a button that silently fires another email on every click is an easy way to accidentally (or deliberately) spam an inbox or a rate-limited API.

Going back without losing context

"Use a different email" clears the countdown timer, swaps back to the entry form, clears the input, and refocuses it — so correcting a typo'd email doesn't require a page reload and doesn't leave a stale countdown silently running in the background after the user has already navigated away from that state.

Why this beats a traditional password field

A password field comes with its own failure modes — weak passwords, reused passwords, forgotten passwords, and the support burden of a reset flow that itself usually emails a link anyway. Magic links collapse all of that into the one flow most users already trust: check email, click link, you're in. The tradeoff is a dependency on email deliverability, which is why the resend control and a clear "check your spam folder" expectation matter as much as the visual design.

Step by step

How to Use

  1. 1
    Paste HTML, CSS, and JSA "Sign in" card renders with an email input and a "Send magic link" button.
  2. 2
    Submit an invalid or empty emailAn inline error message appears below the field without sending anything.
  3. 3
    Submit a valid emailThe button shows "Sending…", then the card swaps to a "Check your email" confirmation showing the address you entered.
  4. 4
    Try resendingThe "Resend link" button is disabled with a live "Resend link in 30s" countdown immediately after sending.
  5. 5
    Wait out the cooldownOnce it reaches 0, the resend button re-enables and can be clicked to restart the cooldown.
  6. 6
    Connect a real email-sending APIReplace the setTimeout in the submit handler with a fetch call to your auth provider's magic-link endpoint, keeping the same UI state transitions.

Real-world uses

Common Use Cases

SaaS and B2B app sign-in
The increasingly standard sign-in pattern for tools like Slack, Notion, and Linear — pair with an OTP input if you also support a code-based fallback.
Newsletter and content gating
Let readers sign in to save articles or comments without ever creating a password.
Low-friction onboarding
Reduce sign-up abandonment by removing the password field from your very first user-facing screen.
Internal tools and admin panels
A quick, password-free sign-in for trusted internal staff using their company email.
Mobile-first web apps
Avoid the password-typing friction that's especially painful on a phone keyboard.
Learning multi-state card patterns
A clear example of toggling between form and confirmation states in one container — compare with a multi-step form for a longer flow.

Got questions?

Frequently Asked Questions

Replace the setTimeout in the submit handler with a fetch POST to your auth provider (Supabase, Auth0, Firebase, or your own backend) passing the validated email; show the check-your-email state only after that request resolves successfully, and show the existing error element if it fails.

The magic link itself points to a callback route in your app containing a one-time token; on that page, verify the token server-side, create a session, and redirect to your authenticated area — this snippet only covers the request side of the flow, not the callback handler.

Mirror the client-side 30-second cooldown with a server-side check (e.g. a timestamp stored per email/IP) so the cooldown can't be bypassed by simply reloading the page and resubmitting, since client-side timers alone are not a security control.

Add a "Sign in with a password instead" link beneath the form that swaps to a password field, or combine with an OTP input for a code-based alternative if email delivery is unreliable for some users.

In React, track the email, error, sent, and cooldown values in useState and run the countdown with setInterval inside useEffect (clearing it on unmount); in Vue, use ref()/onUnmounted for the same cleanup; in Angular, use a component field with ngOnDestroy. The validation regex and state transitions port directly.

Magic Link Login — 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>Magic Link Login</title>
  <style>
    *{box-sizing:border-box;margin:0;padding:0}
    body{font-family:system-ui,-apple-system,sans-serif;background:#f1f5f9;min-height:100vh;display:flex;align-items:center;justify-content:center;padding:24px}
    
    .mll-card{background:#fff;border-radius:18px;padding:28px;width:100%;max-width:360px;box-shadow:0 18px 44px rgba(15,23,42,.12);text-align:center}
    .mll-icon{width:52px;height:52px;border-radius:14px;background:linear-gradient(135deg,#6366f1,#8b5cf6);display:flex;align-items:center;justify-content:center;margin:0 auto 16px}
    
    .mll-card h3{font-size:17px;font-weight:800;color:#0f172a;margin-bottom:7px}
    .mll-card p{font-size:13px;color:#64748b;line-height:1.55;margin-bottom:16px}
    .mll-card p strong{color:#1e293b}
    
    #mllForm input{width:100%;border:1.5px solid #e2e8f0;border-radius:10px;padding:11px 13px;font-size:14px;font-family:inherit;color:#0f172a;margin-bottom:8px;text-align:left;transition:border-color .15s,box-shadow .15s}
    #mllForm input:focus{outline:none;border-color:#6366f1;box-shadow:0 0 0 3px rgba(99,102,241,.15)}
    .mll-error{color:#dc2626;font-size:12px;font-weight:600;text-align:left;margin-bottom:8px !important}
    
    .mll-submit{width:100%;background:#6366f1;color:#fff;border:none;border-radius:10px;padding:12px;font-size:14px;font-weight:700;cursor:pointer;transition:background .15s,opacity .15s;display:flex;align-items:center;justify-content:center;gap:8px}
    .mll-submit:hover{background:#4f46e5}
    .mll-submit:disabled{opacity:.7;cursor:default}
    
    .mll-resend{width:100%;background:transparent;border:1.5px solid #e2e8f0;border-radius:10px;padding:10px;font-size:13.5px;font-weight:700;color:#1e293b;cursor:pointer;margin-bottom:8px;transition:background .15s}
    .mll-resend:hover:not(:disabled){background:#f8fafc}
    .mll-resend:disabled{color:#cbd5e1;cursor:default}
    .mll-back{background:transparent;border:none;color:#94a3b8;font-size:12.5px;font-weight:600;cursor:pointer;padding:6px}
    .mll-back:hover{color:#64748b}
  </style>
</head>
<body>
  <div class="mll-card">
    <div class="mll-icon">
      <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2" 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"/><path d="M22 6l-10 7L2 6"/></svg>
    </div>
  
    <div id="mllForm">
      <h3>Sign in</h3>
      <p>Enter your email and we'll send you a link — no password to remember.</p>
      <input type="email" id="mllEmail" placeholder="[email protected]" autocomplete="email">
      <p class="mll-error" id="mllError" hidden></p>
      <button type="button" class="mll-submit" id="mllSubmit">
        <span id="mllSubmitLabel">Send magic link</span>
      </button>
    </div>
  
    <div id="mllSent" hidden>
      <h3>Check your email</h3>
      <p>We sent a sign-in link to <strong id="mllSentEmail"></strong>. Click it on this device to continue.</p>
      <button type="button" class="mll-resend" id="mllResend">Resend link</button>
      <button type="button" class="mll-back" id="mllBack">Use a different email</button>
    </div>
  </div>
  <script>
    var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    var resendCooldown = 0;
    var cooldownTimer = null;
    
    function isValidEmail(v) { return EMAIL_RE.test(v.trim()); }
    
    document.getElementById('mllSubmit').addEventListener('click', function () {
      var input = document.getElementById('mllEmail');
      var error = document.getElementById('mllError');
      var email = input.value.trim();
    
      if (!email) { error.textContent = 'Enter your email address.'; error.hidden = false; return; }
      if (!isValidEmail(email)) { error.textContent = 'That email address looks invalid.'; error.hidden = false; return; }
      error.hidden = true;
    
      var btn = this, label = document.getElementById('mllSubmitLabel');
      btn.disabled = true; label.textContent = 'Sending…';
    
      setTimeout(function () {
        document.getElementById('mllSentEmail').textContent = email;
        document.getElementById('mllForm').hidden = true;
        document.getElementById('mllSent').hidden = false;
        btn.disabled = false; label.textContent = 'Send magic link';
        startCooldown();
      }, 900);
    });
    
    function startCooldown() {
      resendCooldown = 30;
      var resend = document.getElementById('mllResend');
      resend.disabled = true;
      updateResendLabel();
      clearInterval(cooldownTimer);
      cooldownTimer = setInterval(function () {
        resendCooldown--;
        updateResendLabel();
        if (resendCooldown <= 0) {
          clearInterval(cooldownTimer);
          resend.disabled = false;
          resend.textContent = 'Resend link';
        }
      }, 1000);
    }
    
    function updateResendLabel() {
      var resend = document.getElementById('mllResend');
      resend.textContent = resendCooldown > 0 ? 'Resend link in ' + resendCooldown + 's' : 'Resend link';
    }
    
    document.getElementById('mllResend').addEventListener('click', function () {
      if (this.disabled) return;
      this.textContent = 'Sending…';
      var self = this;
      setTimeout(function () { startCooldown(); }, 600);
    });
    
    document.getElementById('mllBack').addEventListener('click', function () {
      clearInterval(cooldownTimer);
      document.getElementById('mllSent').hidden = true;
      document.getElementById('mllForm').hidden = false;
      document.getElementById('mllEmail').value = '';
      document.getElementById('mllEmail').focus();
    });
  </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>Magic Link Login</title>
  <!-- Tailwind CSS v3+ — arbitrary value classes (e.g. bg-[#0f172a]) are valid -->
  <script src="https://cdn.tailwindcss.com"></script>
  <style>
    * {
      box-sizing:border-box;margin:0;padding:0
    }

    body {
      font-family:system-ui,-apple-system,sans-serif;background:#f1f5f9;min-height:100vh;display:flex;align-items:center;justify-content:center;padding:24px
    }

    .mll-card h3 {
      font-size:17px;font-weight:800;color:#0f172a;margin-bottom:7px
    }

    .mll-card p {
      font-size:13px;color:#64748b;line-height:1.55;margin-bottom:16px
    }

    .mll-card p strong {
      color:#1e293b
    }

    #mllForm input {
      width:100%;border:1.5px solid #e2e8f0;border-radius:10px;padding:11px 13px;font-size:14px;font-family:inherit;color:#0f172a;margin-bottom:8px;text-align:left;transition:border-color .15s,box-shadow .15s
    }

    #mllForm input:focus {
      outline:none;border-color:#6366f1;box-shadow:0 0 0 3px rgba(99,102,241,.15)
    }

    .mll-submit:disabled {
      opacity:.7;cursor:default
    }

    .mll-resend:hover:not(:disabled) {
      background:#f8fafc
    }

    .mll-resend:disabled {
      color:#cbd5e1;cursor:default
    }
  </style>
</head>
<body>
  <div class="mll-card bg-[#fff] rounded-[18px] p-7 w-full max-w-[360px] shadow-[0_18px_44px_rgba(15,23,42,.12)] text-center">
    <div class="w-[52px] h-[52px] rounded-[14px] [background:linear-gradient(135deg,#6366f1,#8b5cf6)] flex items-center justify-center mt-0 mx-auto mb-4">
      <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2" 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"/><path d="M22 6l-10 7L2 6"/></svg>
    </div>
  
    <div id="mllForm">
      <h3>Sign in</h3>
      <p>Enter your email and we'll send you a link — no password to remember.</p>
      <input type="email" id="mllEmail" placeholder="[email protected]" autocomplete="email">
      <p class="text-[#dc2626] text-xs font-semibold text-left mb-[8px !important]" id="mllError" hidden></p>
      <button type="button" class="mll-submit w-full bg-[#6366f1] text-[#fff] border-0 rounded-[10px] p-3 text-sm font-bold cursor-pointer [transition:background_.15s,opacity_.15s] flex items-center justify-center gap-2 hover:bg-[#4f46e5]" id="mllSubmit">
        <span id="mllSubmitLabel">Send magic link</span>
      </button>
    </div>
  
    <div id="mllSent" hidden>
      <h3>Check your email</h3>
      <p>We sent a sign-in link to <strong id="mllSentEmail"></strong>. Click it on this device to continue.</p>
      <button type="button" class="mll-resend w-full bg-transparent border rounded-[10px] p-2.5 text-[13.5px] font-bold text-[#1e293b] cursor-pointer mb-2 [transition:background_.15s]" id="mllResend">Resend link</button>
      <button type="button" class="bg-transparent border-0 text-[#94a3b8] text-[12.5px] font-semibold cursor-pointer p-1.5 hover:text-[#64748b]" id="mllBack">Use a different email</button>
    </div>
  </div>
  <script>
    var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    var resendCooldown = 0;
    var cooldownTimer = null;
    
    function isValidEmail(v) { return EMAIL_RE.test(v.trim()); }
    
    document.getElementById('mllSubmit').addEventListener('click', function () {
      var input = document.getElementById('mllEmail');
      var error = document.getElementById('mllError');
      var email = input.value.trim();
    
      if (!email) { error.textContent = 'Enter your email address.'; error.hidden = false; return; }
      if (!isValidEmail(email)) { error.textContent = 'That email address looks invalid.'; error.hidden = false; return; }
      error.hidden = true;
    
      var btn = this, label = document.getElementById('mllSubmitLabel');
      btn.disabled = true; label.textContent = 'Sending…';
    
      setTimeout(function () {
        document.getElementById('mllSentEmail').textContent = email;
        document.getElementById('mllForm').hidden = true;
        document.getElementById('mllSent').hidden = false;
        btn.disabled = false; label.textContent = 'Send magic link';
        startCooldown();
      }, 900);
    });
    
    function startCooldown() {
      resendCooldown = 30;
      var resend = document.getElementById('mllResend');
      resend.disabled = true;
      updateResendLabel();
      clearInterval(cooldownTimer);
      cooldownTimer = setInterval(function () {
        resendCooldown--;
        updateResendLabel();
        if (resendCooldown <= 0) {
          clearInterval(cooldownTimer);
          resend.disabled = false;
          resend.textContent = 'Resend link';
        }
      }, 1000);
    }
    
    function updateResendLabel() {
      var resend = document.getElementById('mllResend');
      resend.textContent = resendCooldown > 0 ? 'Resend link in ' + resendCooldown + 's' : 'Resend link';
    }
    
    document.getElementById('mllResend').addEventListener('click', function () {
      if (this.disabled) return;
      this.textContent = 'Sending…';
      var self = this;
      setTimeout(function () { startCooldown(); }, 600);
    });
    
    document.getElementById('mllBack').addEventListener('click', function () {
      clearInterval(cooldownTimer);
      document.getElementById('mllSent').hidden = true;
      document.getElementById('mllForm').hidden = false;
      document.getElementById('mllEmail').value = '';
      document.getElementById('mllEmail').focus();
    });
  </script>
</body>
</html>
React
import React, { useEffect } from 'react';

// CSS — optionally move to MagicLinkLogin.module.css
const css = `
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,-apple-system,sans-serif;background:#f1f5f9;min-height:100vh;display:flex;align-items:center;justify-content:center;padding:24px}

.mll-card{background:#fff;border-radius:18px;padding:28px;width:100%;max-width:360px;box-shadow:0 18px 44px rgba(15,23,42,.12);text-align:center}
.mll-icon{width:52px;height:52px;border-radius:14px;background:linear-gradient(135deg,#6366f1,#8b5cf6);display:flex;align-items:center;justify-content:center;margin:0 auto 16px}

.mll-card h3{font-size:17px;font-weight:800;color:#0f172a;margin-bottom:7px}
.mll-card p{font-size:13px;color:#64748b;line-height:1.55;margin-bottom:16px}
.mll-card p strong{color:#1e293b}

#mllForm input{width:100%;border:1.5px solid #e2e8f0;border-radius:10px;padding:11px 13px;font-size:14px;font-family:inherit;color:#0f172a;margin-bottom:8px;text-align:left;transition:border-color .15s,box-shadow .15s}
#mllForm input:focus{outline:none;border-color:#6366f1;box-shadow:0 0 0 3px rgba(99,102,241,.15)}
.mll-error{color:#dc2626;font-size:12px;font-weight:600;text-align:left;margin-bottom:8px !important}

.mll-submit{width:100%;background:#6366f1;color:#fff;border:none;border-radius:10px;padding:12px;font-size:14px;font-weight:700;cursor:pointer;transition:background .15s,opacity .15s;display:flex;align-items:center;justify-content:center;gap:8px}
.mll-submit:hover{background:#4f46e5}
.mll-submit:disabled{opacity:.7;cursor:default}

.mll-resend{width:100%;background:transparent;border:1.5px solid #e2e8f0;border-radius:10px;padding:10px;font-size:13.5px;font-weight:700;color:#1e293b;cursor:pointer;margin-bottom:8px;transition:background .15s}
.mll-resend:hover:not(:disabled){background:#f8fafc}
.mll-resend:disabled{color:#cbd5e1;cursor:default}
.mll-back{background:transparent;border:none;color:#94a3b8;font-size:12.5px;font-weight:600;cursor:pointer;padding:6px}
.mll-back:hover{color:#64748b}
`;

export default function MagicLinkLogin() {
  // 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);
    };
    var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    var resendCooldown = 0;
    var cooldownTimer = null;
    
    function isValidEmail(v) { return EMAIL_RE.test(v.trim()); }
    
    document.getElementById('mllSubmit').addEventListener('click', function () {
      var input = document.getElementById('mllEmail');
      var error = document.getElementById('mllError');
      var email = input.value.trim();
    
      if (!email) { error.textContent = 'Enter your email address.'; error.hidden = false; return; }
      if (!isValidEmail(email)) { error.textContent = 'That email address looks invalid.'; error.hidden = false; return; }
      error.hidden = true;
    
      var btn = this, label = document.getElementById('mllSubmitLabel');
      btn.disabled = true; label.textContent = 'Sending…';
    
      setTimeout(function () {
        document.getElementById('mllSentEmail').textContent = email;
        document.getElementById('mllForm').hidden = true;
        document.getElementById('mllSent').hidden = false;
        btn.disabled = false; label.textContent = 'Send magic link';
        startCooldown();
      }, 900);
    });
    
    function startCooldown() {
      resendCooldown = 30;
      var resend = document.getElementById('mllResend');
      resend.disabled = true;
      updateResendLabel();
      clearInterval(cooldownTimer);
      cooldownTimer = setInterval(function () {
        resendCooldown--;
        updateResendLabel();
        if (resendCooldown <= 0) {
          clearInterval(cooldownTimer);
          resend.disabled = false;
          resend.textContent = 'Resend link';
        }
      }, 1000);
    }
    
    function updateResendLabel() {
      var resend = document.getElementById('mllResend');
      resend.textContent = resendCooldown > 0 ? 'Resend link in ' + resendCooldown + 's' : 'Resend link';
    }
    
    document.getElementById('mllResend').addEventListener('click', function () {
      if (this.disabled) return;
      this.textContent = 'Sending…';
      var self = this;
      setTimeout(function () { startCooldown(); }, 600);
    });
    
    document.getElementById('mllBack').addEventListener('click', function () {
      clearInterval(cooldownTimer);
      document.getElementById('mllSent').hidden = true;
      document.getElementById('mllForm').hidden = false;
      document.getElementById('mllEmail').value = '';
      document.getElementById('mllEmail').focus();
    });
    // 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="mll-card">
        <div className="mll-icon">
          <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#fff" strokeWidth="2" 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"/><path d="M22 6l-10 7L2 6"/></svg>
        </div>
      
        <div id="mllForm">
          <h3>Sign in</h3>
          <p>Enter your email and we'll send you a link — no password to remember.</p>
          <input type="email" id="mllEmail" placeholder="[email protected]" autocomplete="email" />
          <p className="mll-error" id="mllError" hidden></p>
          <button type="button" className="mll-submit" id="mllSubmit">
            <span id="mllSubmitLabel">Send magic link</span>
          </button>
        </div>
      
        <div id="mllSent" hidden>
          <h3>Check your email</h3>
          <p>We sent a sign-in link to <strong id="mllSentEmail"></strong>. Click it on this device to continue.</p>
          <button type="button" className="mll-resend" id="mllResend">Resend link</button>
          <button type="button" className="mll-back" id="mllBack">Use a different email</button>
        </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 MagicLinkLogin() {
  // 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);
    };
    var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    var resendCooldown = 0;
    var cooldownTimer = null;
    
    function isValidEmail(v) { return EMAIL_RE.test(v.trim()); }
    
    document.getElementById('mllSubmit').addEventListener('click', function () {
      var input = document.getElementById('mllEmail');
      var error = document.getElementById('mllError');
      var email = input.value.trim();
    
      if (!email) { error.textContent = 'Enter your email address.'; error.hidden = false; return; }
      if (!isValidEmail(email)) { error.textContent = 'That email address looks invalid.'; error.hidden = false; return; }
      error.hidden = true;
    
      var btn = this, label = document.getElementById('mllSubmitLabel');
      btn.disabled = true; label.textContent = 'Sending…';
    
      setTimeout(function () {
        document.getElementById('mllSentEmail').textContent = email;
        document.getElementById('mllForm').hidden = true;
        document.getElementById('mllSent').hidden = false;
        btn.disabled = false; label.textContent = 'Send magic link';
        startCooldown();
      }, 900);
    });
    
    function startCooldown() {
      resendCooldown = 30;
      var resend = document.getElementById('mllResend');
      resend.disabled = true;
      updateResendLabel();
      clearInterval(cooldownTimer);
      cooldownTimer = setInterval(function () {
        resendCooldown--;
        updateResendLabel();
        if (resendCooldown <= 0) {
          clearInterval(cooldownTimer);
          resend.disabled = false;
          resend.textContent = 'Resend link';
        }
      }, 1000);
    }
    
    function updateResendLabel() {
      var resend = document.getElementById('mllResend');
      resend.textContent = resendCooldown > 0 ? 'Resend link in ' + resendCooldown + 's' : 'Resend link';
    }
    
    document.getElementById('mllResend').addEventListener('click', function () {
      if (this.disabled) return;
      this.textContent = 'Sending…';
      var self = this;
      setTimeout(function () { startCooldown(); }, 600);
    });
    
    document.getElementById('mllBack').addEventListener('click', function () {
      clearInterval(cooldownTimer);
      document.getElementById('mllSent').hidden = true;
      document.getElementById('mllForm').hidden = false;
      document.getElementById('mllEmail').value = '';
      document.getElementById('mllEmail').focus();
    });
    // 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>{`
* {
  box-sizing:border-box;margin:0;padding:0
}

body {
  font-family:system-ui,-apple-system,sans-serif;background:#f1f5f9;min-height:100vh;display:flex;align-items:center;justify-content:center;padding:24px
}

.mll-card h3 {
  font-size:17px;font-weight:800;color:#0f172a;margin-bottom:7px
}

.mll-card p {
  font-size:13px;color:#64748b;line-height:1.55;margin-bottom:16px
}

.mll-card p strong {
  color:#1e293b
}

#mllForm input {
  width:100%;border:1.5px solid #e2e8f0;border-radius:10px;padding:11px 13px;font-size:14px;font-family:inherit;color:#0f172a;margin-bottom:8px;text-align:left;transition:border-color .15s,box-shadow .15s
}

#mllForm input:focus {
  outline:none;border-color:#6366f1;box-shadow:0 0 0 3px rgba(99,102,241,.15)
}

.mll-submit:disabled {
  opacity:.7;cursor:default
}

.mll-resend:hover:not(:disabled) {
  background:#f8fafc
}

.mll-resend:disabled {
  color:#cbd5e1;cursor:default
}
      `}</style>
      <div className="mll-card bg-[#fff] rounded-[18px] p-7 w-full max-w-[360px] shadow-[0_18px_44px_rgba(15,23,42,.12)] text-center">
        <div className="w-[52px] h-[52px] rounded-[14px] [background:linear-gradient(135deg,#6366f1,#8b5cf6)] flex items-center justify-center mt-0 mx-auto mb-4">
          <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#fff" strokeWidth="2" 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"/><path d="M22 6l-10 7L2 6"/></svg>
        </div>
      
        <div id="mllForm">
          <h3>Sign in</h3>
          <p>Enter your email and we'll send you a link — no password to remember.</p>
          <input type="email" id="mllEmail" placeholder="[email protected]" autocomplete="email" />
          <p className="text-[#dc2626] text-xs font-semibold text-left mb-[8px !important]" id="mllError" hidden></p>
          <button type="button" className="mll-submit w-full bg-[#6366f1] text-[#fff] border-0 rounded-[10px] p-3 text-sm font-bold cursor-pointer [transition:background_.15s,opacity_.15s] flex items-center justify-center gap-2 hover:bg-[#4f46e5]" id="mllSubmit">
            <span id="mllSubmitLabel">Send magic link</span>
          </button>
        </div>
      
        <div id="mllSent" hidden>
          <h3>Check your email</h3>
          <p>We sent a sign-in link to <strong id="mllSentEmail"></strong>. Click it on this device to continue.</p>
          <button type="button" className="mll-resend w-full bg-transparent border rounded-[10px] p-2.5 text-[13.5px] font-bold text-[#1e293b] cursor-pointer mb-2 [transition:background_.15s]" id="mllResend">Resend link</button>
          <button type="button" className="bg-transparent border-0 text-[#94a3b8] text-[12.5px] font-semibold cursor-pointer p-1.5 hover:text-[#64748b]" id="mllBack">Use a different email</button>
        </div>
      </div>
    </>
  );
}
Vue
<template>
  <div class="mll-card">
    <div class="mll-icon">
      <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2" 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"/><path d="M22 6l-10 7L2 6"/></svg>
    </div>
  
    <div id="mllForm">
      <h3>Sign in</h3>
      <p>Enter your email and we'll send you a link — no password to remember.</p>
      <input type="email" id="mllEmail" placeholder="[email protected]" autocomplete="email">
      <p class="mll-error" id="mllError" hidden></p>
      <button type="button" class="mll-submit" id="mllSubmit">
        <span id="mllSubmitLabel">Send magic link</span>
      </button>
    </div>
  
    <div id="mllSent" hidden>
      <h3>Check your email</h3>
      <p>We sent a sign-in link to <strong id="mllSentEmail"></strong>. Click it on this device to continue.</p>
      <button type="button" class="mll-resend" id="mllResend">Resend link</button>
      <button type="button" class="mll-back" id="mllBack">Use a different email</button>
    </div>
  </div>
</template>

<script setup>
import { onMounted } from 'vue';

var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
var resendCooldown = 0;
var cooldownTimer = null;
function isValidEmail(v) { return EMAIL_RE.test(v.trim()); }
function startCooldown() {
  resendCooldown = 30;
  var resend = document.getElementById('mllResend');
  resend.disabled = true;
  updateResendLabel();
  clearInterval(cooldownTimer);
  cooldownTimer = setInterval(function () {
    resendCooldown--;
    updateResendLabel();
    if (resendCooldown <= 0) {
      clearInterval(cooldownTimer);
      resend.disabled = false;
      resend.textContent = 'Resend link';
    }
  }, 1000);
}
function updateResendLabel() {
  var resend = document.getElementById('mllResend');
  resend.textContent = resendCooldown > 0 ? 'Resend link in ' + resendCooldown + 's' : 'Resend link';
}

onMounted(() => {
  document.getElementById('mllSubmit').addEventListener('click', function () {
    var input = document.getElementById('mllEmail');
    var error = document.getElementById('mllError');
    var email = input.value.trim();
  
    if (!email) { error.textContent = 'Enter your email address.'; error.hidden = false; return; }
    if (!isValidEmail(email)) { error.textContent = 'That email address looks invalid.'; error.hidden = false; return; }
    error.hidden = true;
  
    var btn = this, label = document.getElementById('mllSubmitLabel');
    btn.disabled = true; label.textContent = 'Sending…';
  
    setTimeout(function () {
      document.getElementById('mllSentEmail').textContent = email;
      document.getElementById('mllForm').hidden = true;
      document.getElementById('mllSent').hidden = false;
      btn.disabled = false; label.textContent = 'Send magic link';
      startCooldown();
    }, 900);
  });
  document.getElementById('mllResend').addEventListener('click', function () {
    if (this.disabled) return;
    this.textContent = 'Sending…';
    var self = this;
    setTimeout(function () { startCooldown(); }, 600);
  });
  document.getElementById('mllBack').addEventListener('click', function () {
    clearInterval(cooldownTimer);
    document.getElementById('mllSent').hidden = true;
    document.getElementById('mllForm').hidden = false;
    document.getElementById('mllEmail').value = '';
    document.getElementById('mllEmail').focus();
  });
});
</script>

<style scoped>
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,-apple-system,sans-serif;background:#f1f5f9;min-height:100vh;display:flex;align-items:center;justify-content:center;padding:24px}

.mll-card{background:#fff;border-radius:18px;padding:28px;width:100%;max-width:360px;box-shadow:0 18px 44px rgba(15,23,42,.12);text-align:center}
.mll-icon{width:52px;height:52px;border-radius:14px;background:linear-gradient(135deg,#6366f1,#8b5cf6);display:flex;align-items:center;justify-content:center;margin:0 auto 16px}

.mll-card h3{font-size:17px;font-weight:800;color:#0f172a;margin-bottom:7px}
.mll-card p{font-size:13px;color:#64748b;line-height:1.55;margin-bottom:16px}
.mll-card p strong{color:#1e293b}

#mllForm input{width:100%;border:1.5px solid #e2e8f0;border-radius:10px;padding:11px 13px;font-size:14px;font-family:inherit;color:#0f172a;margin-bottom:8px;text-align:left;transition:border-color .15s,box-shadow .15s}
#mllForm input:focus{outline:none;border-color:#6366f1;box-shadow:0 0 0 3px rgba(99,102,241,.15)}
.mll-error{color:#dc2626;font-size:12px;font-weight:600;text-align:left;margin-bottom:8px !important}

.mll-submit{width:100%;background:#6366f1;color:#fff;border:none;border-radius:10px;padding:12px;font-size:14px;font-weight:700;cursor:pointer;transition:background .15s,opacity .15s;display:flex;align-items:center;justify-content:center;gap:8px}
.mll-submit:hover{background:#4f46e5}
.mll-submit:disabled{opacity:.7;cursor:default}

.mll-resend{width:100%;background:transparent;border:1.5px solid #e2e8f0;border-radius:10px;padding:10px;font-size:13.5px;font-weight:700;color:#1e293b;cursor:pointer;margin-bottom:8px;transition:background .15s}
.mll-resend:hover:not(:disabled){background:#f8fafc}
.mll-resend:disabled{color:#cbd5e1;cursor:default}
.mll-back{background:transparent;border:none;color:#94a3b8;font-size:12.5px;font-weight:600;cursor:pointer;padding:6px}
.mll-back:hover{color:#64748b}
</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-magic-link-login',
  standalone: true,
  imports: [CommonModule],
  encapsulation: ViewEncapsulation.None,
  template: `
    <div class="mll-card">
      <div class="mll-icon">
        <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2" 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"/><path d="M22 6l-10 7L2 6"/></svg>
      </div>
    
      <div id="mllForm">
        <h3>Sign in</h3>
        <p>Enter your email and we'll send you a link — no password to remember.</p>
        <input type="email" id="mllEmail" placeholder="[email protected]" autocomplete="email">
        <p class="mll-error" id="mllError" hidden></p>
        <button type="button" class="mll-submit" id="mllSubmit">
          <span id="mllSubmitLabel">Send magic link</span>
        </button>
      </div>
    
      <div id="mllSent" hidden>
        <h3>Check your email</h3>
        <p>We sent a sign-in link to <strong id="mllSentEmail"></strong>. Click it on this device to continue.</p>
        <button type="button" class="mll-resend" id="mllResend">Resend link</button>
        <button type="button" class="mll-back" id="mllBack">Use a different email</button>
      </div>
    </div>
  `,
  styles: [`
    *{box-sizing:border-box;margin:0;padding:0}
    body{font-family:system-ui,-apple-system,sans-serif;background:#f1f5f9;min-height:100vh;display:flex;align-items:center;justify-content:center;padding:24px}
    
    .mll-card{background:#fff;border-radius:18px;padding:28px;width:100%;max-width:360px;box-shadow:0 18px 44px rgba(15,23,42,.12);text-align:center}
    .mll-icon{width:52px;height:52px;border-radius:14px;background:linear-gradient(135deg,#6366f1,#8b5cf6);display:flex;align-items:center;justify-content:center;margin:0 auto 16px}
    
    .mll-card h3{font-size:17px;font-weight:800;color:#0f172a;margin-bottom:7px}
    .mll-card p{font-size:13px;color:#64748b;line-height:1.55;margin-bottom:16px}
    .mll-card p strong{color:#1e293b}
    
    #mllForm input{width:100%;border:1.5px solid #e2e8f0;border-radius:10px;padding:11px 13px;font-size:14px;font-family:inherit;color:#0f172a;margin-bottom:8px;text-align:left;transition:border-color .15s,box-shadow .15s}
    #mllForm input:focus{outline:none;border-color:#6366f1;box-shadow:0 0 0 3px rgba(99,102,241,.15)}
    .mll-error{color:#dc2626;font-size:12px;font-weight:600;text-align:left;margin-bottom:8px !important}
    
    .mll-submit{width:100%;background:#6366f1;color:#fff;border:none;border-radius:10px;padding:12px;font-size:14px;font-weight:700;cursor:pointer;transition:background .15s,opacity .15s;display:flex;align-items:center;justify-content:center;gap:8px}
    .mll-submit:hover{background:#4f46e5}
    .mll-submit:disabled{opacity:.7;cursor:default}
    
    .mll-resend{width:100%;background:transparent;border:1.5px solid #e2e8f0;border-radius:10px;padding:10px;font-size:13.5px;font-weight:700;color:#1e293b;cursor:pointer;margin-bottom:8px;transition:background .15s}
    .mll-resend:hover:not(:disabled){background:#f8fafc}
    .mll-resend:disabled{color:#cbd5e1;cursor:default}
    .mll-back{background:transparent;border:none;color:#94a3b8;font-size:12.5px;font-weight:600;cursor:pointer;padding:6px}
    .mll-back:hover{color:#64748b}
  `]
})
export class MagicLinkLoginComponent implements AfterViewInit {
  ngAfterViewInit(): void {
    var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    var resendCooldown = 0;
    var cooldownTimer = null;
    
    function isValidEmail(v) { return EMAIL_RE.test(v.trim()); }
    
    document.getElementById('mllSubmit').addEventListener('click', function () {
      var input = document.getElementById('mllEmail');
      var error = document.getElementById('mllError');
      var email = input.value.trim();
    
      if (!email) { error.textContent = 'Enter your email address.'; error.hidden = false; return; }
      if (!isValidEmail(email)) { error.textContent = 'That email address looks invalid.'; error.hidden = false; return; }
      error.hidden = true;
    
      var btn = this, label = document.getElementById('mllSubmitLabel');
      btn.disabled = true; label.textContent = 'Sending…';
    
      setTimeout(function () {
        document.getElementById('mllSentEmail').textContent = email;
        document.getElementById('mllForm').hidden = true;
        document.getElementById('mllSent').hidden = false;
        btn.disabled = false; label.textContent = 'Send magic link';
        startCooldown();
      }, 900);
    });
    
    function startCooldown() {
      resendCooldown = 30;
      var resend = document.getElementById('mllResend');
      resend.disabled = true;
      updateResendLabel();
      clearInterval(cooldownTimer);
      cooldownTimer = setInterval(function () {
        resendCooldown--;
        updateResendLabel();
        if (resendCooldown <= 0) {
          clearInterval(cooldownTimer);
          resend.disabled = false;
          resend.textContent = 'Resend link';
        }
      }, 1000);
    }
    
    function updateResendLabel() {
      var resend = document.getElementById('mllResend');
      resend.textContent = resendCooldown > 0 ? 'Resend link in ' + resendCooldown + 's' : 'Resend link';
    }
    
    document.getElementById('mllResend').addEventListener('click', function () {
      if (this.disabled) return;
      this.textContent = 'Sending…';
      var self = this;
      setTimeout(function () { startCooldown(); }, 600);
    });
    
    document.getElementById('mllBack').addEventListener('click', function () {
      clearInterval(cooldownTimer);
      document.getElementById('mllSent').hidden = true;
      document.getElementById('mllForm').hidden = false;
      document.getElementById('mllEmail').value = '';
      document.getElementById('mllEmail').focus();
    });

    // Bind inline-handler functions to the component so the template can call them
    Object.assign(this, { isValidEmail, startCooldown, updateResendLabel });
  }
}