Open demo

CSS code

HTML
<div class="lf">
<main class="lf-box">
  <div class="lf-mark" aria-hidden="true">N</div>
  <h1>Sign in to Northwind</h1>
  <p class="lf-sub">Use the passkey saved on this device or your phone. There is no password to remember or leak.</p>
  <button class="lf-pk" type="button" aria-describedby="pk-why">
    <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 11a3 3 0 100-6 3 3 0 000 6z"/><path d="M6 20c0-3 2.5-5 6-5s6 2 6 5"/><path d="M12 15v5"/></svg>
    Continue with a passkey
  </button>
  <p class="lf-why" id="pk-why">Your fingerprint, face or screen lock is checked on your device and never sent to us.</p>
  <div class="lf-or"><span>or</span></div>
  <form onsubmit="event.preventDefault()">
    <label class="lf-label" for="pw-email">Email address</label>
    <input class="lf-field" id="pw-email" name="email" type="email" autocomplete="username webauthn"
           placeholder="[email protected]" required>
    <button class="lf-alt" type="submit">Sign in with a password instead</button>
  </form>
  <p class="lf-foot">No account yet? <a href="#">Create one</a></p>
</main>
</div>
CSS
/* Passkey-first sign-in: one large passkey button, then an email field with autocomplete="username webauthn"
   as the password fallback. Wire the button to navigator.credentials.get(); the "webauthn" token lets the
   browser offer saved passkeys in the email field's autofill when your script requests conditional mediation. */
.lf{--lf-brand:#4f46e5;--lf-brand-dark:#4338ca;--lf-ink:#111827;--lf-muted:#6b7280;--lf-line:#e5e7eb;--lf-edge:#868c99;--lf-bg:#f5f6fa;
  min-height:100vh;display:grid;grid-template-columns:minmax(0,1fr);place-items:center;padding:28px 20px;
  background:var(--lf-bg);color:var(--lf-ink);line-height:normal;
  font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif}
.lf,.lf *,.lf *::before,.lf *::after{box-sizing:border-box}
.lf :where(h1,p,form){margin:0}
.lf :where(input,button){font:inherit}
.lf a{color:var(--lf-brand);text-decoration:none;font-weight:600}
.lf a:hover{text-decoration:underline}
.lf :focus-visible{outline:2px solid var(--lf-brand);outline-offset:2px}
.lf-box{width:100%;max-width:410px;background:#fff;border:1px solid var(--lf-line);border-radius:16px;
  padding:38px 34px;box-shadow:0 10px 34px rgba(17,24,39,.06)}
.lf-mark{width:42px;height:42px;border-radius:12px;background:var(--lf-brand);display:grid;place-items:center;
  color:#fff;font-weight:800;margin-bottom:22px}
.lf h1{font-size:1.4rem;letter-spacing:-.02em;margin-bottom:7px}
.lf-sub{color:var(--lf-muted);font-size:.93rem;line-height:1.6;margin-bottom:26px}
.lf-pk{width:100%;display:flex;align-items:center;justify-content:center;gap:11px;padding:16px;
  border:2px solid var(--lf-brand);border-radius:13px;background:var(--lf-brand);color:#fff;
  font-size:1rem;font-weight:700;cursor:pointer;transition:background .15s,transform .12s}
.lf-pk:hover{background:var(--lf-brand-dark)}
.lf-pk:active{transform:scale(.99)}
.lf-pk svg{width:22px;height:22px;stroke:currentColor;fill:none;stroke-width:1.8}
.lf-why{margin-top:14px;font-size:.84rem;color:var(--lf-muted);line-height:1.65;text-align:center}
.lf-or{display:flex;align-items:center;gap:14px;margin:26px 0;color:var(--lf-muted);font-size:.8rem;
  letter-spacing:.08em;text-transform:uppercase}
.lf-or::before,.lf-or::after{content:"";flex:1;height:1px;background:var(--lf-line)}
.lf-label{display:block;font-size:.85rem;font-weight:600;margin-bottom:6px}
.lf-field{width:100%;padding:12px 14px;border:1px solid var(--lf-edge);border-radius:10px;font-size:.95rem;
  background:#fff;color:inherit;margin-bottom:16px;transition:border-color .15s,box-shadow .15s}
.lf-field::placeholder{color:var(--lf-muted)}
.lf-field:focus{outline:none;border-color:var(--lf-brand);box-shadow:0 0 0 3px rgba(79,70,229,.15)}
.lf-alt{width:100%;padding:13px;border:1px solid var(--lf-line);border-radius:11px;background:#fff;
  font-size:.94rem;font-weight:600;color:var(--lf-ink);cursor:pointer;transition:background .15s}
.lf-alt:hover{background:var(--lf-bg)}
.lf-foot{margin-top:24px;text-align:center;font-size:.89rem;color:var(--lf-muted)}
@media (prefers-reduced-motion:reduce){.lf-pk,.lf-field,.lf-alt{transition:none}.lf-pk:active{transform:none}}

Bootstrap 5 code

Requires: Bootstrap 5.3.8 CSS

HTML
<div class="lf d-grid">
<main class="card w-100">
  <div class="card-body">
    <div class="lf-mark" aria-hidden="true">N</div>
    <h1>Sign in to Northwind</h1>
    <p class="lf-sub text-body-secondary">Use the passkey saved on this device or your phone. There is no password to remember or leak.</p>
    <button class="btn btn-passkey w-100 d-flex align-items-center justify-content-center" type="button" aria-describedby="pk-why">
      <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 11a3 3 0 100-6 3 3 0 000 6z"/><path d="M6 20c0-3 2.5-5 6-5s6 2 6 5"/><path d="M12 15v5"/></svg>
      Continue with a passkey
    </button>
    <p class="lf-why text-body-secondary text-center mb-0" id="pk-why">Your fingerprint, face or screen lock is checked on your device and never sent to us.</p>
    <div class="lf-or d-flex align-items-center text-body-secondary text-uppercase"><span>or</span></div>
    <form onsubmit="event.preventDefault()">
      <label class="form-label" for="pw-email">Email address</label>
      <input class="form-control mb-3" id="pw-email" name="email" type="email" autocomplete="username webauthn"
             placeholder="[email protected]" required>
      <button class="btn btn-alt w-100" type="submit">Sign in with a password instead</button>
    </form>
    <p class="lf-foot text-body-secondary text-center mb-0">No account yet? <a href="#">Create one</a></p>
  </div>
</main>
</div>
CSS
/* Bootstrap .card, .form-control and .btn, recoloured through Bootstrap's CSS variables.
   Wire .btn-passkey to navigator.credentials.get(); "webauthn" in the email field's autocomplete lets the
   browser offer saved passkeys there when your script requests conditional mediation. */
.lf{--lf-brand:#4f46e5;--lf-brand-dark:#4338ca;--lf-ink:#111827;--lf-muted:#6b7280;--lf-line:#e5e7eb;--lf-edge:#868c99;
  --bs-body-color:var(--lf-ink);--bs-secondary-color:var(--lf-muted);--bs-border-color:var(--lf-edge);
  --bs-link-color-rgb:79,70,229;--bs-link-hover-color-rgb:67,56,202;
  min-height:100vh;grid-template-columns:minmax(0,1fr);place-items:center;padding:28px 20px;
  background:#f5f6fa;color:var(--lf-ink);line-height:normal}
.lf a{font-weight:600;text-decoration:none}
.lf a:hover{text-decoration:underline}
.lf :focus-visible{outline:2px solid var(--lf-brand);outline-offset:2px}
.lf .card{--bs-card-border-color:var(--lf-line);--bs-card-border-radius:16px;--bs-card-spacer-y:38px;--bs-card-spacer-x:34px;
  max-width:410px;box-shadow:0 10px 34px rgba(17,24,39,.06)}
.lf-mark{width:42px;height:42px;border-radius:12px;background:var(--lf-brand);display:grid;place-items:center;
  color:#fff;font-weight:800;margin-bottom:22px}
.lf h1{font-size:1.4rem;font-weight:700;line-height:normal;letter-spacing:-.02em;margin-bottom:7px}
.lf-sub{font-size:.93rem;line-height:1.6;margin-bottom:26px}
.lf .btn{transition:background-color .15s,transform .12s}
.lf .btn-passkey{--bs-btn-line-height:normal;--bs-btn-padding-y:16px;--bs-btn-font-size:1rem;--bs-btn-font-weight:700;--bs-btn-border-radius:13px;--bs-btn-border-width:2px;
  --bs-btn-bg:var(--lf-brand);--bs-btn-border-color:var(--lf-brand);--bs-btn-color:#fff;
  --bs-btn-hover-bg:var(--lf-brand-dark);--bs-btn-hover-border-color:var(--lf-brand);--bs-btn-hover-color:#fff;
  --bs-btn-active-bg:var(--lf-brand-dark);--bs-btn-active-border-color:var(--lf-brand);--bs-btn-active-color:#fff;--bs-btn-focus-box-shadow:none;gap:11px}
.lf .btn-passkey:active{transform:scale(.99)}
.lf .btn-passkey svg{width:22px;height:22px;stroke:currentColor;fill:none;stroke-width:1.8}
.lf-why{margin-top:14px;font-size:.84rem;line-height:1.65}
.lf-or{gap:14px;margin:26px 0;font-size:.8rem;letter-spacing:.08em}
.lf-or::before,.lf-or::after{content:"";flex:1;height:1px;background:var(--lf-line)}
.lf .form-label{font-size:.85rem;font-weight:600;margin-bottom:6px}
.lf .form-control{padding:12px 14px;border-radius:10px;font-size:.95rem;line-height:normal;transition:border-color .15s,box-shadow .15s}
.lf .form-control:focus{border-color:var(--lf-brand);box-shadow:0 0 0 3px rgba(79,70,229,.15);outline:none}
.lf .form-control::placeholder{color:var(--lf-muted)}
.lf .btn-alt{--bs-btn-line-height:normal;--bs-btn-padding-y:13px;--bs-btn-font-size:.94rem;--bs-btn-font-weight:600;--bs-btn-border-radius:11px;
  --bs-btn-bg:#fff;--bs-btn-border-color:var(--lf-line);--bs-btn-color:var(--lf-ink);
  --bs-btn-hover-bg:#f5f6fa;--bs-btn-hover-border-color:var(--lf-line);--bs-btn-hover-color:var(--lf-ink);
  --bs-btn-active-bg:#f5f6fa;--bs-btn-active-border-color:var(--lf-line);--bs-btn-active-color:var(--lf-ink);--bs-btn-focus-box-shadow:none}
.lf-foot{margin-top:24px;font-size:.89rem}
@media (prefers-reduced-motion:reduce){.lf .btn,.lf .form-control{transition:none}.lf .btn-passkey:active{transform:none}}

Tailwind 4 code

Requires: Tailwind CSS 4

HTML
<div class="grid min-h-screen grid-cols-[minmax(0,1fr)] place-items-center bg-page px-5 py-7 font-[-apple-system,BlinkMacSystemFont,'Segoe_UI',Roboto,Helvetica,Arial,sans-serif] leading-[normal] text-ink">
<main class="w-full max-w-[410px] rounded-2xl border border-line bg-white px-[34px] py-[38px] shadow-[0_10px_34px_rgba(17,24,39,.06)]">
  <div class="mb-[22px] grid size-[42px] place-items-center rounded-xl bg-brand font-extrabold text-white" aria-hidden="true">N</div>
  <h1 class="mb-[7px] text-[1.4rem] font-bold tracking-[-.02em]">Sign in to Northwind</h1>
  <p class="mb-[26px] text-[.93rem] leading-[1.6] text-muted">Use the passkey saved on this device or your phone. There is no password to remember or leak.</p>
  <button class="flex w-full cursor-pointer items-center justify-center gap-[11px] rounded-[13px] border-2 border-brand bg-brand p-4 text-base font-bold text-white transition-[background-color,transform] duration-150 hover:bg-brand-dark focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand active:scale-[.99] motion-reduce:transition-none motion-reduce:active:scale-100" type="button" aria-describedby="pk-why">
    <svg class="size-[22px] fill-none stroke-current stroke-[1.8]" viewBox="0 0 24 24" aria-hidden="true"><path d="M12 11a3 3 0 100-6 3 3 0 000 6z"/><path d="M6 20c0-3 2.5-5 6-5s6 2 6 5"/><path d="M12 15v5"/></svg>
    Continue with a passkey
  </button>
  <p class="mt-3.5 text-center text-[.84rem] leading-[1.65] text-muted" id="pk-why">Your fingerprint, face or screen lock is checked on your device and never sent to us.</p>
  <div class="my-[26px] flex items-center gap-3.5 text-[.8rem] tracking-[.08em] text-muted uppercase before:h-px before:flex-1 before:bg-line after:h-px after:flex-1 after:bg-line"><span>or</span></div>
  <form onsubmit="event.preventDefault()">
    <label class="mb-1.5 block text-[.85rem] font-semibold" for="pw-email">Email address</label>
    <input class="mb-4 w-full rounded-[10px] border border-edge bg-white px-3.5 py-3 text-[.95rem] transition-[border-color,box-shadow] duration-150 placeholder:text-muted focus:border-brand focus:shadow-[0_0_0_3px_rgba(79,70,229,.15)] focus:outline-none motion-reduce:transition-none"
           id="pw-email" name="email" type="email" autocomplete="username webauthn" placeholder="[email protected]" required>
    <button class="w-full cursor-pointer rounded-[11px] border border-line bg-white p-[13px] text-[.94rem] font-semibold text-ink transition-[background-color,border-color,color] duration-150 hover:bg-page focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand motion-reduce:transition-none" type="submit">Sign in with a password instead</button>
  </form>
  <p class="mt-6 text-center text-[.89rem] text-muted">No account yet? <a class="font-semibold text-brand hover:underline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand" href="#">Create one</a></p>
</main>
</div>
CSS
/* Wire the passkey button to navigator.credentials.get(); "webauthn" in the email field's autocomplete lets the
   browser offer saved passkeys there when your script requests conditional mediation. */
@theme {
  --color-brand: #4f46e5;
  --color-brand-dark: #4338ca;
  --color-ink: #111827;
  --color-muted: #6b7280;
  --color-line: #e5e7eb;
  --color-edge: #868c99;
  --color-page: #f5f6fa;
}

About this set

A sign-in screen that leads with passkeys and keeps a password route one step away. A large filled button reads Continue with a passkey, with a line underneath explaining that the fingerprint, face or screen lock is checked on the device and never sent to the site. An or divider separates it from an email field and an outlined Sign in with a password instead button, and a footer link covers people without an account. Use it when your service already supports WebAuthn and you want most people to take the passwordless path while nobody is locked out. The markup is ready for both passkey flows: wire the button to navigator.credentials.get() for the explicit flow, and the email field carries autocomplete=”username webauthn” so the browser can list saved passkeys in the field’s autofill once your script requests conditional mediation. No WebAuthn code is included, because it depends on your server’s challenge. The Bootstrap version builds the same layout from .card, .form-control and two .btn variants; the Tailwind version uses utilities and a small theme.

What’s included

  • Primary passkey button with an explanatory line tied by aria-describedby
  • Email field with autocomplete="username webauthn" for passkey autofill
  • Password fallback as a secondary outlined button
  • Or divider drawn with pseudo-elements, no extra markup
  • Press feedback that is removed under prefers-reduced-motion

Accessibility

The passkey button has visible text and is described by the privacy line beneath it; the email field has a visible label. Tab moves through the passkey button, the email field, the fallback button and the footer link, each with a 2px outline or a focus ring. Transitions and the pressed scale effect switch off under prefers-reduced-motion.

Customise it

Set --lf-brand and --lf-brand-dark on .lf for the passkey button (plain CSS and Bootstrap, where they also feed .btn-passkey), or edit --color-brand and --color-brand-dark in the Tailwind theme. Button height comes from its 16px padding.