Navigating an unauthenticated user to a dedicated sign-in page when they click a "Sign in" link, start checkout, save an item to favorites, or reach an article paywall causes context loss and increases user drop-off. Immediate UI mode (uiMode: 'immediate' in navigator.credentials.get()) enables contextual quick sign-ins by presenting a unified browser account chooser immediately when local passkeys (publicKey) or saved passwords (password: true) exist on the device.
Unlike standard modal WebAuthn prompts—which always display browser UI and fall back to cross-device QR codes or external security keys—Immediate UI mode fails fast with a NotAllowedError if no eligible credentials are locally available on the device. This lets your application authenticate returning users in place with a single tap while seamlessly transitioning users without local credentials to your standard sign-in page, guest checkout, or inline login form.
Recommended UX patterns and anti-patterns
Trigger Immediate UI mode inside a user-initiated event handler (requiring transient user activation) at moments of clear user intent right before navigating away from the current page. If immediate authentication succeeds, complete the user's intent immediately (whether by updating the current page in place or proceeding directly to their authenticated destination). If it fails with NotAllowedError, show a sign-in or registration flow with a path to complete the user's intent on completion.
Potential use cases:
- Header sign-in link or button interception: Intercept a click on a global "Sign in" link (
event.preventDefault()) to authenticate returning users immediately, or continue navigating to the dedicated sign-in page onNotAllowedError. - Dynamic sign-in before checkout: Authenticate returning shoppers with a single tap when they click "Proceed to checkout" so they land directly on the shipping and payment step, or transition to the standard sign-in or guest checkout screen on fallback.
- Inline sign-in for micro-actions (Favorite, Like, Bookmark, Save): Authenticate in place when a signed-out user clicks a micro-action button and immediately complete the action (for example, toggling
aria-pressed="true"), or open a fallback login modal or sign-in page with a return URL parameter. - In-article soft paywalls: Authenticate readers in place when they click "Continue reading with your account" on a paywall banner, or expand an inline sign-in or registration form on fallback.
Anti-patterns to avoid:
- DO NOT use Immediate UI mode on a dedicated sign-in page: Never attach
uiMode: 'immediate'to the primary "Sign in" or "Sign in with a passkey" button on your final destination sign-in page. Users reach the dedicated sign-in page specifically when they have no local credentials on the current device, or when they intend to use a cross-device passkey (scanning a QR code with a phone) or an external hardware security key. BecauseuiMode: 'immediate'suppresses cross-device QR code and security key options and immediately throwsNotAllowedErrorwhen no local credentials exist, using it on a dedicated sign-in page traps the user in a dead end where clicking the button does nothing. On a dedicated sign-in page, use Conditional UI (mediation: 'conditional'withautocomplete="username webauthn") on form inputs paired with a standard modalnavigator.credentials.get({ publicKey })button (see Passkey Authentication). - DO NOT use Immediate UI mode for step-up reauthentication: Never use
uiMode: 'immediate'to reauthenticate an already signed-in user before sensitive operations (such as changing a password, updating a shipping address, editing saved payment methods, or adding a new passkey). Reauthentication must restrict the prompt to the signed-in user's credentials via a non-emptyallowCredentialslist, which Immediate UI mode rejects withNotAllowedError, and omittingallowCredentialswould let the user inadvertently select a different account than the one currently signed in. For step-up reauthentication, follow Passkey Reauthentication.
Privacy and security constraints
Browsers enforce strict privacy and security rules when uiMode: 'immediate' is requested:
- Transient user activation is required: You MUST call
navigator.credentials.get({ uiMode: 'immediate' })inside a user-initiated event handler (such as aclicklistener on a<button>or<a>element) to prevent silent credential probing. Never invoke it automatically on page load (DOMContentLoaded). Calling the API does not consume the transient activation. - Do not pass a non-empty
allowCredentialslist: Do NOT populatepublicKey.allowCredentialswhenuiMode: 'immediate'is specified. Browsers immediately reject any Immediate UI mode request with a non-emptyallowCredentialslist by throwingNotAllowedErrorso relying parties cannot probe for specific credential IDs to infer prior visits or track users across sessions. - Do not pass an
AbortSignal(signal): Do NOT set thesignalproperty onnavigator.credentials.get()whenuiMode: 'immediate'is specified, as browsers disallow programmatic dismissal of the immediate login dialog. If your page also runs a background Conditional UI (mediation: 'conditional') request, callabort()on that Conditional UIAbortControllerbefore invoking Immediate UI mode, and omitsignalfrom the immediate request itself. - Incognito and private browsing sessions: Requests made in Incognito or private browsing windows always reject with
NotAllowedErrorso private sessions remain indistinguishable from sessions without local credentials.
Server-side setup
Options generation endpoint
Expose an endpoint that generates discoverable WebAuthn request options for unauthenticated visitors:
- Generate a session-bound challenge: Create a cryptographically random challenge buffer, store it in the server session, and encode it as Base64URL.
- Persist user verification preference: Set
userVerification: 'preferred'and storeexpectedUserVerification: 'preferred'in the session. - Omit
allowCredentials: Do not include a non-emptyallowCredentialslist in the options for Immediate UI mode, as browsers reject non-empty allowlists withNotAllowedError.
// Server-side options generation example (/api/login/options)
router.post('/api/login/options', (req, res) => {
const options = {
challenge: serverGeneratedBase64UrlChallenge, // High-entropy random challenge stored in session
rpId: 'example.com',
userVerification: 'preferred',
};
req.session.challenge = options.challenge;
req.session.expectedUserVerification = 'preferred';
return res.json(options);
});
Dual credential verification endpoints
Because navigator.credentials.get({ password: true, publicKey, uiMode: 'immediate' }) can resolve with either a passkey (PublicKeyCredential) or a saved password (PasswordCredential), provide verification handlers for both credential types:
- WebAuthn assertion verification (
/api/login/verify): Verify the assertion against the session challenge and stored public key. If the credential ID is not found in the database, return HTTP status404so the client can notify the password manager viaPublicKeyCredential.signalUnknownCredential(). - Password verification (
/api/login/password): Verify theusername(cred.id) andpassword(cred.password) against the user database and establish an authenticated session.
Client-side implementation
Unified credential request
- Request both passkeys and passwords: Decode the server options using
PublicKeyCredential.parseRequestOptionsFromJSON()and callnavigator.credentials.get()inside a user-initiated click handler withpassword: true,publicKey, anduiMode: 'immediate'(without passingsignal). - Branch verification by
cred.type:- If
cred.type === 'public-key', serialize the assertion withcred.toJSON()and POST it to your WebAuthn verification endpoint. When the server responds with HTTP404, callPublicKeyCredential.signalUnknownCredential({ rpId: publicKey.rpId, credentialId: encoded.id }). - If
cred.type === 'password', POST{ username: cred.id, password: cred.password }to your password verification endpoint.
- If
- Handle
NotAllowedErrorwhen no local credentials exist: CatchNotAllowedError(thrown immediately when no local passkeys or saved passwords exist on the device, or when the user dismisses the chooser) and transition to your standard sign-in flow (such as opening an inline sign-in dialog or navigating to/signinwith a return URL).
let isAuthenticating = false;
async function authenticateWithImmediateUi() {
const optionsResponse = await fetch('/api/login/options', { method: 'POST' });
if (!optionsResponse.ok) {
throw new Error('Failed to fetch authentication options');
}
const optionsJSON = await optionsResponse.json();
const publicKey = PublicKeyCredential.parseRequestOptionsFromJSON(optionsJSON);
// MANDATORY: Request both password and publicKey with uiMode: 'immediate' and NO signal parameter
const cred = await navigator.credentials.get({
password: true,
publicKey,
uiMode: 'immediate',
});
if (!cred) {
throw new DOMException('No credential returned', 'NotAllowedError');
}
if (cred.type === 'public-key') {
const encoded = cred.toJSON();
const verifyResponse = await fetch('/api/login/verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(encoded),
});
if (verifyResponse.status === 404) {
await PublicKeyCredential.signalUnknownCredential({
rpId: publicKey.rpId || window.location.hostname,
credentialId: encoded.id, // Base64URL-encoded credential ID string
});
throw new Error('Passkey credential is no longer recognized by the server');
}
if (!verifyResponse.ok) {
throw new Error('Passkey verification failed');
}
return await verifyResponse.json();
}
if (cred.type === 'password') {
const verifyResponse = await fetch('/api/login/password', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
username: cred.id,
password: cred.password,
}),
});
if (!verifyResponse.ok) {
throw new Error('Password verification failed');
}
return await verifyResponse.json();
}
throw new Error(`Unsupported credential type: ${cred.type}`);
}
// Example: Intercepting an inline micro-action (Favorite button) or Header Sign-in click
const favoriteButton = document.getElementById('favorite-btn');
favoriteButton.addEventListener('click', async (event) => {
event.preventDefault();
if (isAuthenticating) return;
// If already signed in, toggle the micro-action state immediately
if (document.body.dataset.signedIn === 'true') {
const nextPressed = favoriteButton.getAttribute('aria-pressed') !== 'true';
favoriteButton.setAttribute('aria-pressed', String(nextPressed));
return;
}
isAuthenticating = true;
try {
const user = await authenticateWithImmediateUi();
// Complete the user's intended action immediately
document.body.dataset.signedIn = 'true';
favoriteButton.setAttribute('aria-pressed', 'true');
updateHeaderUserState(user);
} catch (err) {
if (err.name === 'NotAllowedError') {
// No local credentials available or user dismissed dialog:
// transition smoothly to fallback sign-in UI or redirect with return URL
showFallbackSignInUi({ returnAction: 'favorite' });
return;
}
console.error('Authentication error:', err);
} finally {
isAuthenticating = false;
}
});
Fallback strategies
Immediate UI mode fallback
- Progressive enhancement experience: Treat
uiMode: 'immediate'as a progressive enhancement. Do not infer support from a browser's name or version; the only reliable support signal is thecapabilities.immediateGetruntime check viaPublicKeyCredential.getClientCapabilities(). MANDATORY: Always checkcapabilities.immediateGetbefore callingnavigator.credentials.get({ uiMode: 'immediate' })—browsers that do not recognizeuiMode: 'immediate'ignore the unknown dictionary key and open a blocking modal passkey and QR code dialog. Whencapabilities.immediateGetis falsy, or whennavigator.credentials.get()rejects withNotAllowedError, fall back to your application's standard sign-in experience (such as opening an inline sign-in modal or navigating to the dedicated sign-in page with a return URL parameter), so users in unsupported browsers see your existing sign-in flow unchanged:
async function isImmediateUiSupported() {
if (!window.PublicKeyCredential || !PublicKeyCredential.getClientCapabilities) {
return false;
}
try {
const capabilities = await PublicKeyCredential.getClientCapabilities();
return Boolean(capabilities?.immediateGet);
} catch {
return false;
}
}
async function authenticateWithImmediateUi() {
// MANDATORY: Check immediateGet support so unsupported browsers do not open a modal QR dialog
const supported = await isImmediateUiSupported();
if (!supported) {
// Throw NotAllowedError so the caller's single catch path treats "unsupported" like "no local credentials"
throw new DOMException('Immediate UI mode is not supported', 'NotAllowedError');
}
// Proceed with fetching options and calling navigator.credentials.get({ password: true, publicKey, uiMode: 'immediate' })
}
Client capabilities detection fallback
Always install 'webauthn-polyfills' and import it in your client module so PublicKeyCredential.getClientCapabilities() is available whenever PublicKeyCredential is supported:
import 'webauthn-polyfills';
Password credentials fallback
Passing password: true alongside publicKey in navigator.credentials.get() allows supporting browsers to include saved passwords from the browser's password manager in the unified immediate account chooser.
- Fallback experience: Browsers that do not support
PasswordCredentialignore thepassword: trueoption or fall back via thecapabilities.immediateGetfeature check to the standard sign-in form, where<input autocomplete="username webauthn">and<input type="password" autocomplete="current-password">provide native password autofill.
Easy JSON serialization fallback
Always install 'webauthn-polyfills' and import it in the client bundle so PublicKeyCredential.parseRequestOptionsFromJSON() and PublicKeyCredential.prototype.toJSON() work consistently across browsers supporting PublicKeyCredential:
import 'webauthn-polyfills';
Signal API synchronization fallback
The WebAuthn Signal API (webauthn-signals) is a progressive enhancement that removes stale passkeys from the user's password manager when the server responds with HTTP 404.
- Fallback experience: Gate calls behind
if (PublicKeyCredential.signalUnknownCredential). When unsupported, skip the signal call gracefully and display a clear message guiding the user to sign in with an alternative method.