Skip to content

Passkey autofill: show passkeys in the username field

Set up passkey autofill with mediation conditional: the autocomplete attribute, the feature check, an empty allowCredentials, aborting for the button, and fixes.

PasskeysBy Rushi Patel, Senior iOS developerUpdated 2 November 20266 min read

Passkey autofill puts a user’s passkeys in the same dropdown as saved passwords, on the username field they already tap. There is no extra button to find and no modal that appears before the user asks. The spec calls it conditional mediation; browsers and docs also call it conditional UI. It takes three changes to a normal passkey sign-in.

1. Mark the username field

Add webauthn as the last token of the field’s autocomplete:

<input type="text" name="username" autocomplete="username webauthn" autofocus />

email webauthn works the same for an email field. Without the webauthn token the browser shows passwords only.

2. Check support

async function autofillSupported() {
  if (!window.PublicKeyCredential) return false;
  if (PublicKeyCredential.getClientCapabilities) {
    const caps = await PublicKeyCredential.getClientCapabilities();
    return caps.conditionalGet === true;
  }
  return PublicKeyCredential.isConditionalMediationAvailable?.() ?? false;
}

getClientCapabilities() is the newer check; isConditionalMediationAvailable() covers browsers that shipped autofill before it.

3. Start a conditional get() when the page loads

Ask your server for sign-in options with an empty allowCredentials, then start the request straight away, not on a click:

const autofill = new AbortController();

async function startAutofill() {
  if (!(await autofillSupported())) return;
  const json = await fetch("/api/passkeys/options").then((r) => r.json()); // fresh challenge, allowCredentials: []
  const publicKey = PublicKeyCredential.parseRequestOptionsFromJSON(json);
  try {
    const cred = await navigator.credentials.get({ publicKey, mediation: "conditional", signal: autofill.signal });
    if (!cred) return;
    await fetch("/api/passkeys/verify", { method: "POST", body: JSON.stringify(cred.toJSON()) });
  } catch (err) {
    if (err.name === "AbortError") return;       // we cancelled it
    if (err.name === "NotAllowedError") return;  // the user dismissed it
    throw err;
  }
}
startAutofill();

The promise stays pending, with no UI, until the user focuses the field and picks a passkey. If they pick a saved password instead, the promise never settles and your normal form submit runs. Your server finds the account from the credential id and userHandle, because it did not know the user when it made the options.

The button, and why you must abort first

Keep a Sign in with a passkey button for browsers without autofill. Only one WebAuthn request runs at a time, so a pending conditional request blocks the button’s call. Abort it first:

button.onclick = async () => {
  autofill.abort();
  const json = await fetch("/api/passkeys/options").then((r) => r.json());
  const cred = await navigator.credentials.get({ publicKey: PublicKeyCredential.parseRequestOptionsFromJSON(json) });
  // verify as above
};

Abort the same way before a registration starts on the same page, and when a single-page app routes away from the sign-in screen.

When passkeys do not appear in the list

Symptom Cause
Only passwords in the dropdown autocomplete lacks webauthn, or get() never ran because it waited for a click
Nothing at all The field is not focused; autofocus or a tap opens the list
The user has a passkey but it is missing rpId differs from the rp.id used at registration, or the passkey was made non-discoverable (residentKey: "discouraged")
Works on desktop, not in the app’s web view Many in-app web views do not support WebAuthn; open the system browser or use the platform’s passkey API
Second sign-in fails with a challenge error The page reused the first challenge. Make a fresh one for every get()
The button does nothing The conditional request is still pending. Abort it before the button’s get()

The server side

  • A new random challenge for every request, kept a few minutes, used once.
  • allowCredentials: [] for autofill, userVerification: "preferred", and the same rpId every time.
  • Look up the credential by id, check the signature, the challenge, the origin and the rpIdHash, then start the session.
  • If the credential id is unknown (the passkey was deleted on your side), answer 404 and, where the browser supports it, call PublicKeyCredential.signalUnknownCredential() so the password manager stops offering it.

With SignOps

SignOps Passkeys makes the options and checks the answer on its servers. In the browser, @signops/web does steps 2 and 3:

const { signops_token } = await passkeys.signIn({ autofill: true });

It checks support, starts the conditional request and gives back a token your server confirms with one call. The full flow, from your domain to the server check, is in the passkeys on your website guide. If something fails, WebAuthn errors lists each error name and its cause, and the passkey checker shows whether this browser supports autofill.

Sources: web.dev: Create a passkey for passwordless logins with form autofill, MDN: CredentialsContainer.get(), mediation, W3C: Web Authentication Level 3.

Frequently asked questions

More questions? Email us and the answer goes on this page.

hello@signops.dev
What is passkey autofill, or conditional UI?

The browser offers the user's saved passkeys in the username field's autofill list, next to saved passwords, instead of in a separate modal. The page starts a navigator.credentials.get() call with mediation set to "conditional" when it loads; the call waits silently until the user picks a passkey from the list.

Why don't my passkeys show in the autofill list?

The usual causes are a missing "webauthn" token in the input's autocomplete attribute, the get() call made with a non-empty allowCredentials list, an rpId that does not match the one the passkey was made with, or the get() call never started because it waited for a click. The field also has to be focused for the list to open.

Should allowCredentials be empty for passkey autofill?

Yes. With conditional mediation the browser lists every discoverable credential for your rpId, so the server does not know the user yet and sends an empty allowCredentials. The passkey itself tells you which user it belongs to through userHandle.

Can I keep a "Sign in with a passkey" button as well?

Yes, and you should, for browsers without autofill support. Only one WebAuthn request can run at a time, so abort the pending conditional request with its AbortController before the button starts its own get().

Send your first push today.

Free for 1,000 push devices, 100 approvals and 1,000 passkey sign-ins a month, and 500 email codes to try. No card until you pass a free amount.