Auth Modal
Headless / custom UI

Headless / custom UI

Build your own login buttons — skip ConnectButton / ConnectModal. Same BYOK adapters and social methods; you call connect yourself.

@web5nexus/authmodal-core@0.3.2@web5nexus/authmodal-react@0.3.8@web5nexus/authmodal-bitcoin@0.1.2

Choose an API

PathWhen to useEntry
React hooksCustom UI inside AuthModalProvideruseConnect() / useAccount() / useDisconnect()
Core clientNon-React or fully headlesscreateAuthModal()client.connect()
Bitcoin clientBTC-first, no EVM modalbitcoinAuth()btc.connect() (Bitcoin Wallet)

Modal theme / layout docs stay on Connect Modal. Adapter install + method matrix: Adapters.

Snippet lab

Loading headless lab…

React — custom buttons

Still wrap the tree in AuthModalProvider (adapters + transport). Omit ConnectButton.

import { AuthModalProvider, useConnect, useAccount, useDisconnect } from '@web5nexus/authmodal-react'
import { web3auth } from '@web5nexus/authmodal-web3auth'
import { magic } from '@web5nexus/authmodal-magic'
 
const adapters = [
  web3auth({ clientId: process.env.NEXT_PUBLIC_W3A_CLIENT_ID! }),
  magic({ apiKey: process.env.NEXT_PUBLIC_MAGIC_API_KEY! }),
]
 
function LoginRow() {
  const { connect, isConnecting, error } = useConnect()
  const { address, isConnected, adapterId } = useAccount()
  const { disconnect } = useDisconnect()
 
  if (isConnected) {
    return (
      <button type="button" onClick={() => void disconnect()}>
        Disconnect {address} ({adapterId})
      </button>
    )
  }
 
  return (
    <div>
      <button
        type="button"
        disabled={isConnecting}
        onClick={() => void connect('google', undefined, 'web3auth')}
      >
        Google · Web3Auth
      </button>
      <button
        type="button"
        disabled={isConnecting}
        onClick={() => void connect('google', undefined, 'magic')}
      >
        Google · Magic
      </button>
      <button
        type="button"
        disabled={isConnecting}
        onClick={() => void connect('email', 'user@example.com', 'web3auth')}
      >
        Email · Web3Auth
      </button>
      {error && <p>{error}</p>}
    </div>
  )
}
 
export default function App() {
  return (
    <AuthModalProvider
      adapters={adapters}
      preferredAdapters={['web3auth', 'magic']}
      chains={[1, 8453]}
      transport={{ mode: 'custom', chainId: 1, rpcUrl: 'https://rpc.ankr.com/eth' }}
    >
      <LoginRow />
    </AuthModalProvider>
  )
}

useConnect signature

connect(
  method: SocialMethod,
  loginHint?: string,   // email | phone | jwt
  adapterId?: string,   // force provider when several share a method
  otpCode?: string,     // after OtpRequiredError
): Promise<SocialSession>

openModal is still available if you want a hybrid (custom CTA → modal).

Core — no React UI

import { createAuthModal, isOtpRequiredError } from '@web5nexus/authmodal-core'
import { web3auth } from '@web5nexus/authmodal-web3auth'
import { magic } from '@web5nexus/authmodal-magic'
 
const client = createAuthModal({
  adapters: [
    web3auth({ clientId: process.env.NEXT_PUBLIC_W3A_CLIENT_ID! }),
    magic({ apiKey: process.env.NEXT_PUBLIC_MAGIC_API_KEY! }),
  ],
  preferredAdapters: ['web3auth', 'magic'],
  chains: [1, 8453],
  transport: { mode: 'custom', chainId: 1, rpcUrl: 'https://rpc.ankr.com/eth' },
})
 
await client.init()
 
const session = await client.connect({
  method: 'google',
  adapterId: 'web3auth',
})
 
await client.setChain(8453)
await client.disconnect()

ConnectOpts:

FieldRole
methodSocial method id
loginHintEmail / phone / JWT when required
otpCodeVerification code after OTP challenge
adapterIdPin provider (web3auth, magic, …)
chainIdOptional target chain for the session

Subscribe with client.subscribe(setState) or read client.getState().

Email OTP flow

Some adapters send a code and throw OtpRequiredError before the session exists:

import { isOtpRequiredError } from '@web5nexus/authmodal-core'
 
try {
  await client.connect({
    method: 'email',
    loginHint: email,
    adapterId: 'web3auth',
  })
} catch (e) {
  if (isOtpRequiredError(e)) {
    // show OTP input, then:
    await client.connect({
      method: 'email',
      loginHint: e.email,
      otpCode: codeFromUser,
      adapterId: e.adapterId,
    })
  } else {
    throw e
  }
}

React: same args on connect(method, loginHint, adapterId, otpCode). Challenge also appears on useAuthModal().state.otpChallenge when using the modal path.

Social methods — when loginHint is required

MethodloginHintNotes
google, apple, discord, twitter, github, facebook, linkedin, twitchNoProvider OAuth / redirect
emailEmail addressOften followed by OTP
phoneE.164 phoneWeb3Auth primarily
jwtJWT stringWeb3Auth custom auth
passkeyNoWeb3Auth passkey

Always pass adapterId when multiple adapters share the same method (e.g. Google on Web3Auth and Magic).

Per-provider headless

Enable only the methods you need via each factory’s socialMethods. Full catalog:

Web3Auth@web5nexus/authmodal-web3auth@0.2.5

Key export: Yes · Bitcoin: Yes

GoogleAppleEmailPhoneDiscordX / TwitterGitHubFacebookLinkedInTwitchPasskeyJWT
Magic@web5nexus/authmodal-magic@0.2.3

Key export: No · Bitcoin: No

EmailGoogleAppleGitHubFacebookX / TwitterDiscordLinkedInTwitch
Privy@web5nexus/authmodal-privy@0.1.3

Key export: No · Bitcoin: No

EmailGoogleAppleDiscordX / TwitterGitHubLinkedInTwitch
Particle@web5nexus/authmodal-particle@0.1.4

Key export: No · Bitcoin: No

EmailGoogleAppleDiscordX / TwitterGitHubFacebookLinkedInTwitch
Para@web5nexus/authmodal-para@0.1.3

Key export: No · Bitcoin: No

EmailGoogleAppleDiscordFacebookX / Twitter

Web3Auth — @web5nexus/authmodal-web3auth

import { web3auth } from '@web5nexus/authmodal-web3auth'
 
web3auth({
  clientId: process.env.NEXT_PUBLIC_W3A_CLIENT_ID!,
  network: 'sapphire_devnet',
  socialMethods: ['google', 'apple', 'email', 'discord', 'twitter', 'github'],
})
 
await client.connect({ method: 'google', adapterId: 'web3auth' })
await client.connect({
  method: 'email',
  loginHint: 'user@example.com',
  adapterId: 'web3auth',
})

Vite: import @web5nexus/authmodal-web3auth/polyfill first. Key export required for Bitcoin derive.

Magic — @web5nexus/authmodal-magic

import { magic } from '@web5nexus/authmodal-magic'
 
magic({
  apiKey: process.env.NEXT_PUBLIC_MAGIC_API_KEY!,
  socialMethods: ['email', 'google', 'apple'],
})
 
await client.connect({ method: 'email', loginHint: 'user@example.com', adapterId: 'magic' })
await client.connect({ method: 'google', adapterId: 'magic' })

Privy — @web5nexus/authmodal-privy

import { privy } from '@web5nexus/authmodal-privy'
 
privy({
  appId: process.env.NEXT_PUBLIC_PRIVY_APP_ID!,
  socialMethods: ['email', 'google', 'apple', 'discord'],
})
 
await connect('google', undefined, 'privy')
await connect('email', 'user@example.com', 'privy')

Particle — @web5nexus/authmodal-particle

import { particle } from '@web5nexus/authmodal-particle'
 
particle({
  projectId: process.env.NEXT_PUBLIC_PARTICLE_PROJECT_ID!,
  clientKey: process.env.NEXT_PUBLIC_PARTICLE_CLIENT_KEY!,
  appId: process.env.NEXT_PUBLIC_PARTICLE_APP_ID!,
  socialMethods: ['email', 'google', 'apple'],
})
 
await connect('google', undefined, 'particle')

May need Particle WASM Vite plugin — see Auth Modal demo config.

Para — @web5nexus/authmodal-para

import { para } from '@web5nexus/authmodal-para'
 
para({
  apiKey: process.env.NEXT_PUBLIC_PARA_API_KEY!,
  socialMethods: ['email', 'google', 'apple'],
})
 
await connect('apple', undefined, 'para')

Bitcoin headless (Web3Auth only)

import { bitcoinAuth } from '@web5nexus/authmodal-bitcoin'
 
const btc = bitcoinAuth({
  clientId: process.env.NEXT_PUBLIC_W3A_CLIENT_ID!,
  bitcoinNetwork: 'testnet',
  addressTypes: ['taproot', 'segwit', 'legacy', 'nested-segwit'],
  prefer: 'taproot',
})
 
await btc.init()
const session = await btc.connect({ method: 'google' })
// session.addresses.primary — bc1p… / tb1p…
 
await btc.connect({ method: 'email', loginHint: 'user@example.com' })

React without the Bitcoin modal: BitcoinWalletProvider + useBitcoinWallet().connect({ method }). Details: Bitcoin Wallet.

Live reference

Auth Modal demo (Vite, port 5177):

  • Core headlesscreateAuthModal + email/Google per adapter
  • Per-adapter panels — every social method for Web3Auth / Magic / Privy / Particle / Para

Docs labs stay snippet-only (Next does not bundle provider SDKs).

Next