Open demo

CSS code

HTML
<div class="mg">
  <div class="mg-card" data-magic>
    <div data-step="ask">
      <div class="mg-icon"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M7.5 5.6 9 3l1.5 2.6L13 7l-2.5 1.4L9 11 7.5 8.4 5 7l2.5-1.4zM18 9l1 1.8L21 12l-2 1.2L18 15l-1-1.8L15 12l2-1.2L18 9zM10.5 12.9 3 20.4 4.6 22l7.5-7.5-1.6-1.6z"/></svg></div>
      <h1>Sign in without a password</h1>
      <p class="mg-sub">Enter your email and we’ll send you a one-time magic link. No password to remember, nothing to reset.</p>
      <form action="#" method="post">
        <label class="mg-label" for="mg-email">Email address</label>
        <input class="mg-input" type="email" id="mg-email" name="email" placeholder="[email protected]" autocomplete="username" required>
        <button class="mg-btn" type="submit">Email me a magic link</button>
      </form>
      <p class="mg-hint">Prefer a password? <a href="#">Use classic login</a></p>
    </div>
    <div class="mg-done" data-step="sent" hidden>
      <div class="mg-icon is-ok"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M20 4H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2zm0 4-8 5-8-5V6l8 5 8-5v2z"/></svg></div>
      <h1 tabindex="-1">Check your inbox</h1>
      <p>We sent a sign-in link to <strong data-sent-to></strong>. It expires in 15 minutes.</p>
      <p class="mg-resend">Nothing arrived? <button class="mg-link" type="button" data-again>Try again</button></p>
    </div>
  </div>
</div>
CSS
/* Passwordless login: one email field, then a "check your inbox" step swapped in with the hidden attribute. */
.mg{--mg-ink:#0f172a;--mg-muted:#64748b;--mg-sky:#0ea5e9;--mg-accent:#0369a1;--mg-accent-dark:#075985;--mg-line:#8a94a6;
  min-height:100vh;display:flex;align-items:center;justify-content:center;padding:24px;background:radial-gradient(1200px 600px at 50% -10%,#e0f2fe,#f8fafc);
  color:var(--mg-ink);font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif}
.mg h1,.mg p{margin:0}
.mg [hidden]{display:none!important}
.mg-card{width:100%;max-width:400px;background:#fff;border:1px solid #e2e8f0;border-radius:18px;padding:44px 38px;box-shadow:0 18px 40px -16px rgba(15,23,42,.15);text-align:center}
.mg-icon{width:56px;height:56px;margin:0 auto 18px;border-radius:16px;background:var(--mg-sky);display:flex;align-items:center;justify-content:center;box-shadow:0 10px 22px -8px rgba(14,165,233,.6)}
.mg-icon svg{width:28px;height:28px;fill:#fff}
.mg-icon.is-ok{background:#10b981;box-shadow:0 10px 22px -8px rgba(16,185,129,.55)}
.mg h1{font-size:1.45rem;letter-spacing:-.02em;margin-bottom:8px}
.mg h1:focus{outline:none}
.mg-sub{color:var(--mg-muted);font-size:.92rem;line-height:1.55;margin-bottom:22px!important}
.mg-label{display:block;font-size:.85rem;font-weight:600;margin-bottom:8px}
.mg-input{display:block;width:100%;padding:13px 15px;border:1px solid var(--mg-line);border-radius:12px;font:inherit;font-size:.95rem;color:inherit;text-align:center;
  background:#fff;transition:border-color .15s,box-shadow .15s}
.mg-input::placeholder{color:var(--mg-muted)}
.mg-input:focus{outline:none;border-color:#0284c7;box-shadow:0 0 0 3px rgba(14,165,233,.25)}
.mg-btn{width:100%;margin-top:14px;padding:13px;border:0;border-radius:12px;background:var(--mg-accent);color:#fff;font:inherit;font-weight:700;font-size:.95rem;
  cursor:pointer;transition:background-color .15s}
.mg-btn:hover{background:var(--mg-accent-dark)}
.mg-hint{margin-top:20px!important;font-size:.8rem;color:var(--mg-muted)}
.mg-done p{color:var(--mg-muted);font-size:.92rem;line-height:1.6}
.mg-done strong{color:var(--mg-ink);overflow-wrap:anywhere}
.mg .mg-resend{margin-top:18px;font-size:.85rem}
.mg a,.mg-link{color:var(--mg-accent);font-weight:600;text-decoration:none;border-radius:3px}
.mg-link{padding:0;border:0;background:none;font:inherit;cursor:pointer}
.mg a:hover,.mg-link:hover{text-decoration:underline}
.mg a:focus-visible,.mg-link:focus-visible,.mg-btn:focus-visible{outline:2px solid var(--mg-accent);outline-offset:2px}
@media (prefers-reduced-motion:reduce){.mg-input,.mg-btn{transition:none}}
JavaScript
// Magic link, front end only: after the browser's own email check passes, swap to the "check your inbox" step.
// Nothing is sent. Post the address to your auth endpoint inside the submit handler.
document.querySelectorAll('[data-magic]').forEach(function (card) {
  var ask = card.querySelector('[data-step="ask"]');
  var sent = card.querySelector('[data-step="sent"]');
  var form = ask.querySelector('form');
  var email = form.querySelector('input[type="email"]');
  form.addEventListener('submit', function (e) {
    e.preventDefault();
    sent.querySelector('[data-sent-to]').textContent = email.value;
    ask.hidden = true;
    sent.hidden = false;
    sent.querySelector('h1').focus();
  });
  sent.querySelector('[data-again]').addEventListener('click', function () {
    sent.hidden = true;
    ask.hidden = false;
    email.focus();
  });
});

Bootstrap 5 code

Requires: Bootstrap 5.3.8 CSS

HTML
<div class="mg d-flex align-items-center justify-content-center">
  <div class="card w-100 text-center" data-magic>
    <div class="card-body">
      <div data-step="ask">
        <div class="mg-icon d-flex align-items-center justify-content-center mx-auto"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M7.5 5.6 9 3l1.5 2.6L13 7l-2.5 1.4L9 11 7.5 8.4 5 7l2.5-1.4zM18 9l1 1.8L21 12l-2 1.2L18 15l-1-1.8L15 12l2-1.2L18 9zM10.5 12.9 3 20.4 4.6 22l7.5-7.5-1.6-1.6z"/></svg></div>
        <h1>Sign in without a password</h1>
        <p class="mg-sub text-body-secondary">Enter your email and we’ll send you a one-time magic link. No password to remember, nothing to reset.</p>
        <form action="#" method="post">
          <label class="form-label" for="mg-email">Email address</label>
          <input class="form-control text-center" type="email" id="mg-email" name="email" placeholder="[email protected]" autocomplete="username" required>
          <button class="btn btn-mg w-100" type="submit">Email me a magic link</button>
        </form>
        <p class="mg-hint text-body-secondary mb-0">Prefer a password? <a href="#">Use classic login</a></p>
      </div>
      <div class="mg-done" data-step="sent" hidden>
        <div class="mg-icon is-ok d-flex align-items-center justify-content-center mx-auto"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M20 4H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2zm0 4-8 5-8-5V6l8 5 8-5v2z"/></svg></div>
        <h1 tabindex="-1">Check your inbox</h1>
        <p class="text-body-secondary mb-0">We sent a sign-in link to <strong data-sent-to></strong>. It expires in 15 minutes.</p>
        <p class="mg-resend text-body-secondary mb-0">Nothing arrived? <button class="btn btn-link" type="button" data-again>Try again</button></p>
      </div>
    </div>
  </div>
</div>
CSS
/* Bootstrap .card with a centred .form-control and .btn; the sent step is a sibling shown with the hidden attribute. */
.mg{--mg-sky:#0ea5e9;--mg-accent:#0369a1;--mg-accent-dark:#075985;
  --bs-body-color:#0f172a;--bs-secondary-color:#64748b;--bs-border-color:#8a94a6;--bs-link-color-rgb:3,105,161;--bs-link-hover-color-rgb:3,105,161;
  --bs-body-font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
  min-height:100vh;padding:24px;background:radial-gradient(1200px 600px at 50% -10%,#e0f2fe,#f8fafc);color:var(--bs-body-color);
  font-family:var(--bs-body-font-family);line-height:normal}
.mg [hidden]{display:none!important}
.mg .card{--bs-card-border-color:#e2e8f0;--bs-card-border-radius:18px;--bs-card-spacer-y:44px;--bs-card-spacer-x:38px;max-width:400px;
  box-shadow:0 18px 40px -16px rgba(15,23,42,.15)}
.mg-icon{width:56px;height:56px;margin-bottom:18px;border-radius:16px;background:var(--mg-sky);box-shadow:0 10px 22px -8px rgba(14,165,233,.6)}
.mg-icon svg{width:28px;height:28px;fill:#fff}
.mg-icon.is-ok{background:#10b981;box-shadow:0 10px 22px -8px rgba(16,185,129,.55)}
.mg h1{font-size:1.45rem;letter-spacing:-.02em;font-weight:700;margin-bottom:8px}
.mg h1:focus{outline:none}
.mg-sub{font-size:.92rem;line-height:1.55;margin-bottom:22px}
.mg .form-label{font-size:.85rem;font-weight:600;margin-bottom:8px}
.mg .form-control{padding:13px 15px;border-radius:12px;font-size:.95rem;line-height:1.2}
.mg .form-control::placeholder{color:var(--bs-secondary-color)}
.mg .form-control:focus{border-color:#0284c7;box-shadow:0 0 0 3px rgba(14,165,233,.25)}
.mg .btn-mg{--bs-btn-bg:var(--mg-accent);--bs-btn-border-color:var(--mg-accent);--bs-btn-color:#fff;--bs-btn-hover-bg:var(--mg-accent-dark);
  --bs-btn-hover-border-color:var(--mg-accent-dark);--bs-btn-hover-color:#fff;--bs-btn-active-bg:var(--mg-accent-dark);--bs-btn-active-border-color:var(--mg-accent-dark);
  --bs-btn-active-color:#fff;--bs-btn-padding-y:13px;--bs-btn-font-size:.95rem;--bs-btn-font-weight:700;--bs-btn-border-radius:12px;--bs-btn-line-height:1.2;
  --bs-btn-focus-box-shadow:none;margin-top:14px}
.mg-hint{margin-top:20px;font-size:.8rem}
.mg-done p{font-size:.92rem;line-height:1.6}
.mg-done strong{color:var(--bs-body-color);overflow-wrap:anywhere}
.mg-resend{margin-top:18px;font-size:.85rem!important}
.mg a,.mg .btn-link{font-weight:600;text-decoration:none;border-radius:3px}
.mg .btn-link{--bs-btn-padding-x:0;--bs-btn-padding-y:0;--bs-btn-font-size:inherit;--bs-btn-border-width:0;--bs-btn-color:var(--mg-accent);
  --bs-btn-hover-color:var(--mg-accent);--bs-btn-active-color:var(--mg-accent);--bs-btn-focus-box-shadow:none;vertical-align:baseline;line-height:inherit}
.mg a:hover,.mg .btn-link:hover{text-decoration:underline}
.mg a:focus-visible,.mg .btn:focus-visible{outline:2px solid var(--mg-accent);outline-offset:2px}
@media (prefers-reduced-motion:reduce){.mg .form-control,.mg .btn{transition:none}}
JavaScript
// Magic link, front end only: after the browser's own email check passes, swap to the "check your inbox" step.
// Nothing is sent. Post the address to your auth endpoint inside the submit handler.
document.querySelectorAll('[data-magic]').forEach(function (card) {
  var ask = card.querySelector('[data-step="ask"]');
  var sent = card.querySelector('[data-step="sent"]');
  var form = ask.querySelector('form');
  var email = form.querySelector('input[type="email"]');
  form.addEventListener('submit', function (e) {
    e.preventDefault();
    sent.querySelector('[data-sent-to]').textContent = email.value;
    ask.hidden = true;
    sent.hidden = false;
    sent.querySelector('h1').focus();
  });
  sent.querySelector('[data-again]').addEventListener('click', function () {
    sent.hidden = true;
    ask.hidden = false;
    email.focus();
  });
});

Tailwind 4 code

Requires: Tailwind CSS 4

HTML
<div class="flex min-h-screen items-center justify-center bg-[radial-gradient(1200px_600px_at_50%_-10%,#e0f2fe,#f8fafc)] p-6 font-[-apple-system,BlinkMacSystemFont,'Segoe_UI',Roboto,Helvetica,Arial,sans-serif] leading-[normal] text-mg-ink">
  <div class="w-full max-w-[400px] rounded-[18px] border border-[#e2e8f0] bg-white px-[38px] py-11 text-center shadow-[0_18px_40px_-16px_rgba(15,23,42,.15)]" data-magic>
    <div data-step="ask">
      <div class="mx-auto mb-[18px] flex size-14 items-center justify-center rounded-2xl bg-mg-sky shadow-[0_10px_22px_-8px_rgba(14,165,233,.6)]"><svg class="size-7 fill-white" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M7.5 5.6 9 3l1.5 2.6L13 7l-2.5 1.4L9 11 7.5 8.4 5 7l2.5-1.4zM18 9l1 1.8L21 12l-2 1.2L18 15l-1-1.8L15 12l2-1.2L18 9zM10.5 12.9 3 20.4 4.6 22l7.5-7.5-1.6-1.6z"/></svg></div>
      <h1 class="mb-2 text-[1.45rem] font-bold tracking-[-.02em] focus:outline-none">Sign in without a password</h1>
      <p class="mb-[22px] text-[.92rem] leading-[1.55] text-mg-muted">Enter your email and we’ll send you a one-time magic link. No password to remember, nothing to reset.</p>
      <form action="#" method="post">
        <label class="mb-2 block text-[.85rem] font-semibold" for="mg-email">Email address</label>
        <input class="block w-full rounded-xl border border-mg-line bg-white px-[15px] py-[13px] text-center text-[.95rem] transition-[border-color,box-shadow] duration-150 placeholder:text-mg-muted focus:border-[#0284c7] focus:shadow-[0_0_0_3px_rgba(14,165,233,.25)] focus:outline-none motion-reduce:transition-none" type="email" id="mg-email" name="email" placeholder="[email protected]" autocomplete="username" required>
        <button class="mt-3.5 w-full cursor-pointer rounded-xl bg-mg-accent p-[13px] text-[.95rem] font-bold text-white transition-colors duration-150 hover:bg-mg-accent-dark focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-mg-accent motion-reduce:transition-none" type="submit">Email me a magic link</button>
      </form>
      <p class="mt-5 text-[.8rem] text-mg-muted">Prefer a password? <a class="rounded-[3px] font-semibold text-mg-accent hover:underline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-mg-accent" href="#">Use classic login</a></p>
    </div>
    <div data-step="sent" hidden>
      <div class="mx-auto mb-[18px] flex size-14 items-center justify-center rounded-2xl bg-[#10b981] shadow-[0_10px_22px_-8px_rgba(16,185,129,.55)]"><svg class="size-7 fill-white" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M20 4H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2zm0 4-8 5-8-5V6l8 5 8-5v2z"/></svg></div>
      <h1 class="mb-2 text-[1.45rem] font-bold tracking-[-.02em] focus:outline-none" tabindex="-1">Check your inbox</h1>
      <p class="text-[.92rem] leading-[1.6] text-mg-muted">We sent a sign-in link to <strong class="wrap-anywhere text-mg-ink" data-sent-to></strong>. It expires in 15 minutes.</p>
      <p class="mt-[18px] text-[.85rem] text-mg-muted">Nothing arrived? <button class="cursor-pointer rounded-[3px] font-semibold text-mg-accent hover:underline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-mg-accent" type="button" data-again>Try again</button></p>
    </div>
  </div>
</div>
CSS
@theme {
  --color-mg-ink: #0f172a;
  --color-mg-muted: #64748b;
  --color-mg-sky: #0ea5e9;
  --color-mg-accent: #0369a1;
  --color-mg-accent-dark: #075985;
  --color-mg-line: #8a94a6;
}
JavaScript
// Magic link, front end only: after the browser's own email check passes, swap to the "check your inbox" step.
// Nothing is sent. Post the address to your auth endpoint inside the submit handler.
document.querySelectorAll('[data-magic]').forEach(function (card) {
  var ask = card.querySelector('[data-step="ask"]');
  var sent = card.querySelector('[data-step="sent"]');
  var form = ask.querySelector('form');
  var email = form.querySelector('input[type="email"]');
  form.addEventListener('submit', function (e) {
    e.preventDefault();
    sent.querySelector('[data-sent-to]').textContent = email.value;
    ask.hidden = true;
    sent.hidden = false;
    sent.querySelector('h1').focus();
  });
  sent.querySelector('[data-again]').addEventListener('click', function () {
    sent.hidden = true;
    ask.hidden = false;
    email.focus();
  });
});

About this set

A passwordless sign-in card in two steps. The first step shows a wand icon, a heading, a line explaining that a one-time link will be emailed, a centred email field and an Email me a magic link button, plus a link back to classic login. When the address passes the browser’s own email check, a small script hides that step and shows the second: a green envelope icon, a Check your inbox heading, the address the link went to, and a Try again button that returns to the form. Focus moves to the new heading so screen reader users hear the change, and Try again puts focus back in the email field. Nothing is sent: the script marks where to post the address to your auth endpoint. Use it for newsletters, communities and low-friction apps where users forget passwords. Compared with the original, the email label is now visible, Try again is a real button instead of a link without a destination, and the sky-blue button and links were deepened to #0369a1 so white and blue text meet 4.5:1. The Bootstrap version uses .card, .form-control, .btn and .btn-link.

What’s included

  • Two steps swapped with the hidden attribute and about twenty lines of script
  • Focus moves to the Check your inbox heading and back to the field
  • Uses the browser's built-in email validation
  • Try again is a real button, not an empty link
  • Accent deepened to #0369a1 to meet WCAG AA

Accessibility

The email field has a visible label and autocomplete="username". After sending, focus moves to the new heading; Try again returns focus to the email field. Buttons and links show a 2px blue outline on keyboard focus and transitions switch off under prefers-reduced-motion.

Customise it

Change --mg-accent, --mg-accent-dark and --mg-sky on .mg; --mg-sky is only the icon tile. In Tailwind edit the --color-mg-* theme colours.