web3icons

Designing the connect wallet flow

When to ask for a wallet connection, how to list and label wallets, EIP-6963 discovery, mobile deep links and QR codes, and remembering the last wallet.

5 min read

The connect wallet flow is the first real interaction most people have with a dApp, and it involves software you don't control: browser extensions, mobile apps and relay protocols. Your part is deciding when to ask, presenting the choices clearly, and handling the different paths on desktop and mobile without dead ends.

This guide covers timing, the wallet list, multi-wallet discovery with EIP-6963, mobile connections, and what to do on the next visit.

Ask when the action needs it

A connection request is a request for trust. It goes better when the person already knows what the app does and has a reason to say yes.

Most dApps can show nearly everything without a wallet: prices, pools, collections, a swap form with a quote. Put the connect prompt on the action that needs an account, not on the whole page. A swap button that reads "Connect wallet" before connection and "Swap" after is clearer than a modal that blocks the page on load.

Two exceptions are worth noting. Pages that are only about the person's own data, like a portfolio view, can lead with the connect prompt, as long as they also allow pasting an address or ENS name to view any account read-only. And if the visitor arrived from a wallet's in-app browser, a wallet is already present, so connecting can be a single tap.

What the wallet list should show

The modal's job is to help someone find the wallet they already use in a couple of seconds. That comes down to three things.

Recognizable icons with names. People find their wallet by its icon first, then confirm with the name. Show both, every time. A 32px icon with the name in body text works well in a list; 40px to 48px suits a grid of tiles. Keep the icons at a consistent size and padding so no brand looks more prominent than another. You can find the main ones in the wallet category, including MetaMask, Rainbow and Ledger.

Detected wallets first. If a wallet's extension is installed, put it at the top and mark it, for example with an "Installed" label. This is the most likely choice.

A path for people without a wallet. "I don't have a wallet" should lead somewhere useful: a short explanation and a few options, not a list of thirty brands.

Keep the visible list short. Four to six options plus a "More wallets" entry is easier to scan than a long grid. If you support connection through a QR-based protocol such as WalletConnect, list it as one option that leads to the QR code, so people aren't left wondering whether their wallet is supported.

For the modal's semantics, focus handling and labels, see making wallet connection modals accessible.

Discovering wallets with EIP-6963

When several wallet extensions are installed, they used to compete for the single window.ethereum object, and often whichever loaded last won. EIP-6963 fixes this with browser events. Each wallet announces itself with a provider and an info object containing a uuid, a name, an icon as a data URI, and an rdns (a reverse-domain identifier such as io.metamask).

const wallets = new Map()

window.addEventListener('eip6963:announceProvider', (event) => {
  const { info, provider } = event.detail
  wallets.set(info.uuid, { info, provider })
  renderWalletList([...wallets.values()])
})

// Ask wallets that loaded before this script to announce again
window.dispatchEvent(new Event('eip6963:requestProvider'))

The provider is a standard EIP-1193 provider, so connecting is the same call for every wallet:

const [account] = await provider.request({ method: 'eth_requestAccounts' })

The announced icon is supplied by the wallet, so render it with an <img> element. An SVG loaded through <img> doesn't run scripts, which matters when the image comes from code you didn't write. Because the name is shown as text right next to it, the icon itself can have empty alt text:

type WalletInfo = { uuid: string; name: string; icon: string; rdns: string }

function WalletOption({ info, onSelect }: { info: WalletInfo; onSelect: () => void }) {
  return (
    <button type="button" onClick={onSelect} className="wallet-option">
      <img src={info.icon} alt="" width={32} height={32} />
      <span>{info.name}</span>
    </button>
  )
}

Use the announced icon for detected wallets and your own icon set for wallets that aren't installed. If you mix the two, size them identically. Wallet teams should know what to ship for this; what makes a good wallet icon covers it.

Mobile is a different flow

Mobile browsers generally don't run wallet extensions, so there is usually no injected provider in mobile Safari or Chrome. People connect in one of two ways:

Path How it works What to design
Wallet's in-app browser The dApp is opened inside the wallet app, which injects a provider Detect the provider and connect in one tap
Pairing with a wallet app A connection protocol pairs the page with the wallet app through a relay On mobile, a button that opens the wallet app; on desktop, a QR code

The rule that follows: don't show a QR code to someone on a phone. They can't scan their own screen. On mobile, show the wallet list and open the chosen wallet with a deep link or universal link. On desktop, the QR code is the right default for phone wallets, with the code large enough to scan from arm's length (around 250px to 300px is comfortable) and a "Copy link" fallback beneath it.

Most mobile wallets document a link format that opens a URL in their in-app browser. Check each wallet's developer documentation for the exact format, since they differ and change over time. After the wallet app returns control to the browser, show a clear "Waiting for approval in your wallet" state with a retry option, because the hand-off between apps can fail silently.

Remembering the last wallet

On a return visit, reconnecting should feel automatic, but it must not open a wallet popup on page load. Two calls behave differently here:

  • eth_requestAccounts asks the person to connect and may open a wallet popup.
  • eth_accounts returns the accounts this site is already authorized for, or an empty array, without prompting.

Store the wallet's rdns after a successful connection. On the next visit, wait for that wallet to announce itself, then check quietly:

const LAST_WALLET = 'lastWalletRdns'

async function restoreConnection(wallets) {
  const rdns = localStorage.getItem(LAST_WALLET)
  const match = [...wallets.values()].find((w) => w.info.rdns === rdns)
  if (!match) return null

  const accounts = await match.provider.request({ method: 'eth_accounts' })
  return accounts.length > 0 ? { wallet: match, account: accounts[0] } : null
}

If the check returns nothing, show the normal Connect button, perhaps with the remembered wallet first in the list. Clear the stored value when the person disconnects in your interface.

After the connection

Once connected, replace the Connect button with the account: a short address or ENS name, the wallet icon, and the current network. Clicking it should open a small menu with copy address, view on explorer, switch wallet and disconnect. Listen for accountsChanged and chainChanged events on the provider, and update the interface when the person changes account or network in the wallet itself.

If the connected network isn't one your app supports, keep the account visible and ask to switch at the point of action, as described in network switching without confusion.

More in Web3 UX

All guides