LoginModal
Login and signup modal with email, OTP verification, and social authentication options.
pnpm add @wandercom/design-system-web
import { LoginModal } from '@wandercom/design-system-web/blocks/login-modal';
import { Button } from '@wandercom/design-system-web/ui/button';
export function Example() {
return (
<LoginModal
trigger={<Button>Log in</Button>}
onEmailSubmit={(email) => sendOtp(email)}
onCodeSubmit={(code) => verifyCode(code)}
onResendCode={() => resendOtp()}
onGoogleLogin={() => signInWithGoogle()}
onAppleLogin={() => signInWithApple()}
onFacebookLogin={() => signInWithFacebook()}
onPhoneSubmit={(phone) => sendOtp(phone)}
/>
);
}Submitting an email transitions directly to a 6-digit OTP verification step. Clicking the phone button first prompts for a phone number, then transitions to OTP verification. The verification description adapts based on the method used.
Callbacks that return a Promise are handled automatically — the modal shows a loading state during the async operation, advances to the next step on resolve, and displays the error message on reject.
<LoginModal
trigger={<Button>Log in</Button>}
onEmailSubmit={async (email) => {
const { digitsKey } = await sendMagicEmail(email);
store.setDigitsKey(digitsKey);
}}
onCodeSubmit={async (code) => {
const session = await checkMagicDigits(code, digitsKey);
setUserSession(session);
}}
/>On the verification step, clicking "Resend code." confirms the request in place —
the prefix becomes "New code sent." and the button counts down, staying disabled
until the cooldown expires so the endpoint can't be spammed. If onResendCode
returns a Promise, the button reads "Sending…" while it is in flight, and the
confirmation and cooldown only start once it resolves; a rejection is shown as an
error instead.
<LoginModal
onCodeSubmit={(code) => verifyCode(code)}
onResendCode={async () => await resendOtp(email)}
resendCooldownSeconds={60}
/>All of the strings involved (codeSent, sendingCode, resendCooldownTemplate)
are overridable via the labels prop.
The cooldown resets when the user goes back from the verification step or the
modal closes, alongside the rest of the resend state — a resend still in flight
at that point no longer applies its confirmation or cooldown. Without an
onResendCode handler the button is inert: nothing is confirmed and no cooldown
starts.
For forced authentication flows, set closable={false} to prevent the user from dismissing the modal.
<LoginModal
open={mustAuthenticate}
closable={false}
onEmailSubmit={(email) => sendOtp(email)}
onCodeSubmit={(code) => verifyCode(code)}
/><LoginModal
trigger={<Button>Join now</Button>}
title="Welcome to Wander"
description="Join thousands of travelers worldwide."
/>