Supporting Passkeys Across Browsers and Platforms

The WebAuthn API is standard; what users actually experience is not. Availability, prompt design, synchronisation behaviour and error reporting all differ by platform, and a passkey rollout that assumes one behaviour produces confusing dead ends on the others. This page is part of the implementing passkeys and WebAuthn guide.

Detect Capabilities, Do Not Sniff Browsers

The platform exposes exactly the questions you need to ask, so never branch on a user-agent string — it is wrong within a release cycle and it misses the cases that matter, such as a desktop browser that can use a phone as an authenticator.

const supported = typeof window.PublicKeyCredential !== "undefined";

const [platformAuthenticator, conditionalUi] = supported
  ? await Promise.all([
      PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable(),
      PublicKeyCredential.isConditionalMediationAvailable?.() ?? false,
    ])
  : [false, false];

Those three answers drive the whole interface. supported decides whether to offer passkeys at all. platformAuthenticator tells you whether this device can create one itself — a laptop without biometrics may still use a security key or a phone, so a false answer is not “no passkeys”. And conditionalUi decides whether you can offer passkeys in the autofill dropdown rather than behind a button.

Cache the answers per session rather than per page load, and re-evaluate after enrolment, because a user who has just registered a credential should immediately see the passkey path on subsequent visits.

Conditional UI Is the Feature Users Notice

Conditional mediation — the passkey appearing in the username field’s autofill list — is the difference between passkeys feeling like a shortcut and feeling like an extra step. Where it is available, request it on page load and let the browser surface the credential naturally.

Conditional passkey autofill versus a button-triggered flow With conditional mediation the credential appears in the autofill list and one tap signs the user in; without it the user must choose a passkey button before the platform prompt appears. Conditional UI credential appears in autofill one tap, no extra screen highest completion rate Button-triggered user must choose "use a passkey" then the platform prompt works everywhere, converts less
Implement both. The conditional request runs quietly in the background and the button remains for platforms and users that need it.

The conditional request must be started before the user focuses the field and abandoned cleanly if they type a password instead — use an AbortController and cancel it when another authentication path begins, or the outstanding request can block a later call.

Cross-Device Sign-In Changes the Mental Model

A user on a desktop without a platform authenticator can still authenticate by scanning a QR code with a phone that holds the passkey. That flow is genuinely useful and it changes what “this device does not support passkeys” means — usually it means “this device cannot create one”, not “this user cannot use one here”.

Word your interface accordingly. Offering “use a passkey from another device” rather than hiding the option entirely is the difference between a user completing sign-in and falling back to a password they may not remember.

Expect the flow to be slower and occasionally to fail on restricted networks, since it depends on a proximity check between the two devices. Give it a generous timeout, show clear progress, and always leave the fallback path visible while it runs.

Errors Differ, So Map Them Deliberately

The specification defines a small set of error names, and platforms use them with different emphasis. Mapping them to user-facing messages is what turns a dead end into a next step.

WebAuthn error names and what they usually mean NotAllowedError covers cancellation and timeout, InvalidStateError means the credential already exists on this authenticator, SecurityError indicates a relying party identifier mismatch, and NotSupportedError means the requested parameters are unavailable. NotAllowedError cancelled or timed out — offer to try again InvalidStateError already enrolled here — say so, do not retry SecurityError rpID does not match the origin — your bug NotSupportedError parameters unavailable — widen the algorithm list
Only the third row is a defect in your configuration; the others are conditions your interface should handle gracefully.

NotAllowedError is deliberately vague — it covers a user dismissing the prompt, a timeout, and several conditions the platform declines to distinguish for privacy reasons. Do not try to infer more from it than it says; offer a retry and keep the alternative path visible.

Enrolment Across Several Devices

Users end up with credentials on more than one device, and how you handle that decides how many support contacts you get.

Send the user’s existing credential identifiers in excludeCredentials during registration so the platform politely declines to enrol the same authenticator twice rather than creating a confusing duplicate. Where the platform reports InvalidStateError, present it as “this device already has a passkey for your account”, not as a failure.

Encourage a second credential immediately after the first, while the concept is fresh, and show a device list with names, platforms and last-used dates. Synced passkeys mean one enrolment often covers several of a user’s devices already — surface that fact, because users who think each device needs its own enrolment give up partway through.

Measuring the Rollout by Platform

Metrics to break down by platform during a passkey rollout Track enrolment completion, sign-in completion and fallback rate separately for each platform, because a problem on one platform is invisible in the aggregate. Enrolment completion started versus finished per platform Sign-in share passkey versus password the headline number Fallback rate gave up and used a password where the problems are
An aggregate that looks healthy routinely hides one platform where enrolment fails for everybody — the breakdown is what makes that visible.

Attach the error name to the fallback metric so a platform-specific spike identifies itself. A run of NotAllowedError on one browser usually means the prompt is appearing without enough explanation; a run of SecurityError means a configuration mismatch that only affects the origins that browser is being used on, such as a staging host with a different relying-party identifier.

Watch the second-credential rate as closely as the first. Users with exactly one passkey generate the lockouts and the manual recovery requests, so the share of enrolled users holding two or more is the number that predicts your future support load — and it responds well to a prompt at the right moment rather than to any amount of documentation.

Fallbacks That Do Not Undermine the Point

A passkey rollout is only as strong as what happens when it fails. Keeping a password as the fallback is reasonable during adoption; keeping SMS as the fallback for a phishing-resistant credential is not, because the account’s real strength becomes the weakest path.

Decide the ladder explicitly: passkey first, then another passkey, then a verified recovery path, and only then anything weaker — with the weaker options restricted to regaining access rather than authorising sensitive operations. The recovery strategies guide covers sizing that ladder to the account.

Instrument every step. The share of sign-ins completed with a passkey, the rate of each error name, and the fallback rate per platform together tell you whether the rollout is working — and which platform needs attention — far more reliably than anecdotes from colleagues who all use the same laptop.

Testing on Real Devices

Emulators reproduce the API and not the experience, which is where the differences live. Keep a small device matrix and run the same four checks on each: enrol a credential, sign in with conditional autofill, sign in with the button path, and cancel the prompt to confirm the error handling.

Cover at minimum a recent version of each major desktop browser, one iOS device, one Android device, and one hardware security key on a desktop without a platform authenticator. That last combination catches the assumption that “no platform authenticator” means “no passkeys”, which is the most common way a rollout excludes users unnecessarily.

Repeat the matrix after any change to your relying-party identifier, your origin list, or your enrolment flow — and after major browser releases, since prompt behaviour and conditional-mediation support are exactly the sort of thing that changes between versions without any announcement that reaches your team.

Record the results somewhere shared. Passkey behaviour questions come up repeatedly during a rollout, and a table showing what actually happened on each platform ends arguments that would otherwise be settled by whoever has the loudest anecdote.

Frequently Asked Questions

How do I know whether to show the passkey option?

Ask the platform rather than the user agent: PublicKeyCredential for basic support, isUserVerifyingPlatformAuthenticatorAvailable() for a built-in authenticator, and isConditionalMediationAvailable() for autofill. Remember that a false answer to the second does not mean no passkeys — the user may have one on a phone and use cross-device sign-in, so keep that option offered rather than hidden.

Why does the same code behave differently on iOS and Android?

Because the platform owns the prompt, the credential store and much of the error reporting. Timeouts, whether a credential syncs, and how a cancellation is reported all vary. Write to the specification, handle the documented error names, and test on real devices for each platform you support — an emulator will not reproduce the prompt behaviour that users actually meet.

Should I request attestation to identify the authenticator?

For consumer products, no. It adds a privacy-sensitive prompt on some platforms, complicates the ceremony, and yields information you will not act on. Enterprise deployments restricting authentication to approved hardware are the genuine case, and there you verify the statement against the metadata service and accept that personal devices will be refused.

What if a user's passkey does not appear on a new device?

Either the credential is device-bound rather than synced, or the platform’s keychain has not synchronised. Show whether each credential is backed up in your device list so the user can see which of their passkeys travel, and make enrolling an additional one on the new device a single obvious action rather than a support conversation.

Can I require a passkey and remove passwords entirely?

Eventually, and only for accounts with at least two credentials or a verified recovery path. Removing the password is a one-way door for users whose devices break, so gate it on evidence that the account can still be recovered, and keep the option to re-add a password if a user’s circumstances change. Adoption first, enforcement second.