Implementation
Passkey autofill with conditional UI: show passkeys in the username field
5 min read
Key points
- Conditional UI starts a pending WebAuthn request when the login page loads and lists passkeys among the username field's autofill suggestions.
- Add autocomplete="username webauthn" to the field, check support, then call navigator.credentials.get() with mediation 'conditional'.
- Users without a passkey carry on with their password, so you can add it without redesigning the login page.
Conditional UI (conditional mediation) shows passkeys next to saved passwords in the autofill suggestions of your login page's username field. Add autocomplete="username webauthn" to the field and call navigator.credentials.get() with mediation: 'conditional' when the page loads; when the user focuses the field, any passkeys for your site appear as suggestions. Picking one completes sign-in, and users who pick a password carry on as before. The appeal is that you can add it without changing how your login page looks.
What happens
A regular "Sign in with a passkey" button opens the browser's authentication dialog as soon as it's pressed. Users without a passkey see the dialog too, and only then learn that this device has none.
Conditional mediation changes the order. The W3C WebAuthn spec says that when mediation is "conditional", prominent modal UI should not be shown unless credentials are discovered. You can start the request as the page loads and nothing appears; when the user focuses the field, usable passkeys on the device show up as autofill suggestions.
| Button sign-in | Autofill (conditional UI) | |
|---|---|---|
| When you call it | When the button is pressed | When the page loads |
| Dialog | Appears immediately | Not shown until the user picks a suggestion |
| Users without a passkey | See a "not found" message | Nothing happens; they use their password |
| How long it waits | Until the user acts | Until a suggestion is picked (possibly until they leave the page) |
Implementation steps
1. Add the autocomplete token
<input type="text" name="username" autocomplete="username webauthn" />The web.dev guide says webauthn should be the last token in the value.
2. Check support
if (window.PublicKeyCredential &&
PublicKeyCredential.isConditionalMediationAvailable &&
await PublicKeyCredential.isConditionalMediationAvailable()) {
startConditionalLogin();
}PublicKeyCredential.isConditionalMediationAvailable() resolves to whether conditional mediation is available. The web.dev guide also shows the newer PublicKeyCredential.getClientCapabilities(). Where it isn't supported, show only the button and the password login.
3. Fetch a challenge and start waiting
async function startConditionalLogin() {
const res = await fetch('/api/passkey/login-options', { method: 'POST' });
const json = await res.json();
const publicKey = PublicKeyCredential.parseRequestOptionsFromJSON(json.options);
abortController = new AbortController();
const credential = await navigator.credentials.get({
publicKey,
mediation: 'conditional',
signal: abortController.signal,
});
await fetch('/api/passkey/login-verify', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify(credential.toJSON()),
});
}Your server issues the challenge without knowing the user, and allowCredentials stays empty because you don't yet know who is signing in. Look the user up from the returned credential ID (and userHandle), then verify the signature.
4. Make it abortable
A conditional request doesn't finish until the user picks a suggestion. Abort it with AbortController when:
- The user presses the "Sign in with a passkey" button (abort before calling
navigator.credentials.get()for the button) - An SPA navigates to another view
- The user signed in with a password
The web.dev guide also recommends an AbortController for when the user picks a password or leaves the page.
Common pitfalls
Only discoverable credentials can appear
Because the autofill request leaves allowCredentials empty, the only candidates are passkeys the device can find without a username: discoverable credentials. The WebAuthn spec defines a discoverable credential as one usable in ceremonies where the RP provides no credential IDs. Credentials registered with residentKey: 'discouraged' may not show up. If autofill will be your main entry point, request residentKey: 'required' at registration.
The field is missing or rendered late
If there's no field with autocomplete="username webauthn" when you make the call, there is nowhere to show suggestions. If your framework renders the field later, call after it renders.
Challenge expiry
Users can leave the login page open for a long time. If your server puts an expiry on challenges, it may have passed by the time a suggestion is picked. When verification fails because the challenge expired, restart the wait with a new challenge.
Keep the button
Autofill is invisible until the user focuses the field. A visible "Sign in with a passkey" button next to it helps users with passkeys find their way in.
Don't break the password flow
When the user picks a password, the conditional promise simply never resolves and the normal password sign-in continues. Make sure your password submission doesn't depend on the pending passkey request.
Implementing with SealGate
SealGate's browser SDK lets you start and abort the autofill wait in one line each.
<input name="username" autocomplete="username webauthn" />import { SealGate } from 'https://sealgate.jp/sdk/v1.js';
const sg = new SealGate({
optionsUrl: '/api/passkey/options',
verifyUrl: '/api/passkey/verify',
});
if (await SealGate.conditionalMediationAvailable()) {
sg.login({}, { mediation: 'conditional' })
.then(onSignedIn)
.catch((e) => { if (e.code !== 'aborted') console.error(e); });
}
// when switching to button sign-in, or on SPA navigation
SealGate.abort();- Your server calls
POST /v1/login/optionswithoutexternal_id. When the user picks a suggestion, the SDK sends the authenticator's response to your verifyUrl; your server relays it toPOST /v1/login/verifyand gets back theexternal_id. - An aborted call fails with
SealGateErrorandcode: 'aborted'. If autofill isn't supported or the matching field is missing, the code isunsupported. - Challenges expire after 5 minutes and can be used once. If one expires while waiting, verify returns 410
challenge_expired; callsg.login({}, { mediation: 'conditional' })again. - If you rely only on autofill, set
resident_keytorequiredin the environment settings (the default ispreferred). With an API key you can change it throughPATCH /v1/apps/{app_id}.
See the SDK section of the docs for details.
FAQ
Do I still need a passkey button if I add autofill?
We recommend keeping it. Autofill stays hidden until the field is focused, so a button helps users with passkeys find the way in.
What do users without a passkey see?
Nothing. No passkey appears in the suggestions, and they sign in with their password as usual.
Which passkeys show up in the suggestions?
Discoverable credentials for your RP ID on that device or in its password manager. The request names no user, so only passkeys the device can find on its own are candidates.