LoginModal

Login and signup modal with email, OTP verification, and social authentication options.

Installation

pnpm add @wandercom/design-system-web

Usage

Loading example...
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.

Async callbacks

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);
  }}
/>

Non-closable mode

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)}
/>

Custom title

<LoginModal
  trigger={<Button>Join now</Button>}
  title="Welcome to Wander"
  description="Join thousands of travelers worldwide."
/>

Props

open?:

boolean
Controlled open state of the modal.

onOpenChange?:

(open: boolean) => void
Callback when the open state changes.

defaultOpen?:

boolean
Uncontrolled default open state.

trigger?:

ReactNode
Optional trigger element that opens the modal when clicked.

title?:

string
Title text displayed in the modal header. Defaults to "Log in or sign up".

description?:

string
Description text displayed below the title.

onEmailSubmit?:

(email: string) => void | Promise<void>
Callback when the email form is submitted. Returns a Promise for async loading and error handling. Transitions to OTP verification on resolve.

onCodeSubmit?:

(code: string) => void | Promise<void>
Callback when the OTP verification code is submitted. Returns a Promise for async loading and error handling. Closes modal on resolve.

onResendCode?:

() => void
Callback when the resend code link is clicked.

onGoogleLogin?:

() => void
Callback when the Google login button is clicked.

onAppleLogin?:

() => void
Callback when the Apple login button is clicked.

onFacebookLogin?:

() => void
Callback when the Facebook login button is clicked.

onPhoneSubmit?:

(phone: string) => void | Promise<void>
Callback when the phone number is submitted. Returns a Promise for async loading and error handling. Transitions to OTP verification on resolve.

companyName?:

string
Company name displayed in the terms footer. Falls back to "our" if not provided. Defaults to "Wander".

termsUrl?:

string
URL for the Terms of Service link. Defaults to "/terms".

privacyUrl?:

string
URL for the Privacy Policy link. Defaults to "/privacy".

className?:

string
Additional CSS classes to apply to the modal content.

error?:

string
Error message to display. Takes precedence over internally managed errors from rejected async callbacks.

closable?:

boolean
Whether the modal can be dismissed via close button, overlay click, or escape key. Set to false for forced authentication flows. Defaults to true.

onBack?:

() => void
Callback when the user navigates back from the verification step.
LoginModal