Web3Modal Frontend Integration
If your dApp needs to support multiple wallets, don't build a custom wallet selector from scratch. WalletConnect's Web3Modal v3 solves this: 300+ wallets, WalletConnect QR, mobile deeplinks, email/social login via Web3Auth — all out of the box. The integration challenge is proper config setup and avoiding typical pitfalls with hydration and SSR.
Installation and Basic Configuration
npm install @web3modal/wagmi wagmi viem @tanstack/react-query
Get your Project ID from cloud.walletconnect.com. Without it, the modal will launch, but WalletConnect connections won't work.
// config/web3modal.ts
import { createWeb3Modal } from '@web3modal/wagmi/react'
import { defaultWagmiConfig } from '@web3modal/wagmi/react/config'
import { mainnet, arbitrum, base, polygon } from 'wagmi/chains'
const projectId = import.meta.env.VITE_WC_PROJECT_ID
const metadata = {
name: 'My dApp',
description: 'My dApp description',
url: 'https://mydapp.xyz', // must match the domain in WC Cloud
icons: ['https://mydapp.xyz/icon.png'],
}
export const config = defaultWagmiConfig({
chains: [mainnet, arbitrum, base, polygon],
projectId,
metadata,
})
createWeb3Modal({
wagmiConfig: config,
projectId,
enableAnalytics: true,
enableOnramp: true, // built-in fiat on-ramp
themeMode: 'dark',
themeVariables: {
'--w3m-accent': '#7c3aed',
'--w3m-border-radius-master': '4px',
},
})
Integration into React Application
// main.tsx
import { WagmiProvider } from 'wagmi'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { config } from './config/web3modal'
const queryClient = new QueryClient()
export function App() {
return (
<WagmiProvider config={config}>
<QueryClientProvider client={queryClient}>
<Router />
</QueryClientProvider>
</WagmiProvider>
)
}
The connect button is either a ready-made component or custom via a hook:
// Ready-made component
<w3m-button />
// Custom button
import { useWeb3Modal } from '@web3modal/wagmi/react'
import { useAccount } from 'wagmi'
function ConnectWallet() {
const { open } = useWeb3Modal()
const { address, isConnected } = useAccount()
return (
<button onClick={() => open()}>
{isConnected ? `${address?.slice(0, 6)}...${address?.slice(-4)}` : 'Connect Wallet'}
</button>
)
}
Custom Networks
Adding networks not available in wagmi/chains:
import { defineChain } from 'viem'
const sonic = defineChain({
id: 146,
name: 'Sonic',
nativeCurrency: { name: 'Sonic', symbol: 'S', decimals: 18 },
rpcUrls: { default: { http: ['https://rpc.soniclabs.com'] } },
blockExplorers: { default: { name: 'SonicScan', url: 'https://sonicscan.org' } },
})
export const config = defaultWagmiConfig({
chains: [mainnet, sonic],
// ...
})
Network Switching
Web3Modal shows a network switcher automatically. For programmatic switching:
import { useWeb3Modal } from '@web3modal/wagmi/react'
const { open } = useWeb3Modal()
// Open directly on the networks tab
<button onClick={() => open({ view: 'Networks' })}>Switch Network</button>
SSR / Next.js
In Next.js App Router, hydration is an issue: Web3Modal initializes client-side, but server rendering doesn't know about the connected wallet. Solution: 'use client' directive for the provider and lazy initialization:
// providers/Web3Provider.tsx
'use client'
import { createWeb3Modal } from '@web3modal/wagmi/react'
import { useEffect, useState } from 'react'
// Initialization outside the component — executes once
createWeb3Modal({ wagmiConfig: config, projectId })
export function Web3Provider({ children }: { children: React.ReactNode }) {
const [mounted, setMounted] = useState(false)
useEffect(() => setMounted(true), [])
return (
<WagmiProvider config={config}>
<QueryClientProvider client={queryClient}>
{mounted ? children : null}
</QueryClientProvider>
</WagmiProvider>
)
}
The mounted guard prevents hydration mismatch — without it, React throws a warning about the difference between server and client rendering of the wallet address.
Email and Social Login
Web3Modal supports embedded wallets via Web3Auth. Enable in config:
createWeb3Modal({
// ...
featuredWalletIds: [], // remove featured wallets if you want a clean UI
emailEnabled: true, // email OTP wallet
})
Users log in via email or Google/GitHub — they get a non-custodial wallet. For a broad audience without crypto experience, this significantly reduces the barrier to entry.







