← Back to articles
June 202612 min read

Wallet UX Best Practices for Modern Web3 Apps

Learn how to design wallet connection flows, handle network switching, manage disconnected states, and build trust in Web3 products. Practical patterns for dashboards and DeFi apps with wagmi v2 code examples.

Web3UXWagmiWallet

Why wallet UX makes or breaks Web3 products

Wallet connection is not a login button — it is the first trust checkpoint. Users decide within seconds whether your app feels professional, safe, and worth connecting to. Poor wallet UX is the number one reason Web3 dashboards lose users before they see any data.

A strong wallet experience covers connection, network switching, transaction feedback, and graceful recovery from every failure mode. Get these right and your product feels native to crypto users. Get them wrong and even great backend architecture cannot save the first impression.

The wallet connection flow

Keep the connect action visible but not aggressive. Place a clear "Connect Wallet" button in the header. On click, show immediate feedback — a loading state within 100ms — before the wallet modal appears. Users interpret silence as a broken app.

Use wagmi v2 with ConnectKit or RainbowKit for production apps. They handle wallet detection, mobile deep links, and reconnection on page refresh.

// components/ConnectButton.tsx
'use client';

import { useAccount, useConnect, useDisconnect } from 'wagmi';
import { useState } from 'react';

export function ConnectButton() {
  const { address, isConnected } = useAccount();
  const { connect, connectors, isPending } = useConnect();
  const { disconnect } = useDisconnect();
  const [showModal, setShowModal] = useState(false);

  if (isConnected && address) {
    return (
      <button onClick={() => disconnect()} className="btn-secondary">
        {address.slice(0, 6)}…{address.slice(-4)}
      </button>
    );
  }

  return (
    <>
      <button
        onClick={() => setShowModal(true)}
        disabled={isPending}
        className="btn-primary"
      >
        {isPending ? 'Connecting…' : 'Connect Wallet'}
      </button>
      {showModal && (
        <WalletModal
          connectors={connectors}
          onSelect={(connector) => {
            connect({ connector });
            setShowModal(false);
          }}
          onClose={() => setShowModal(false)}
        />
      )}
    </>
  );
}

Network switching

Never assume the user is on the correct chain. Detect mismatches early and offer a one-click switch. Blocking the entire dashboard with an error page is worse than showing a targeted banner.

// components/NetworkGuard.tsx
'use client';

import { useAccount, useChainId, useSwitchChain } from 'wagmi';
import { SUPPORTED_CHAIN_ID } from '@/lib/chains';

export function NetworkGuard({ children }: { children: React.ReactNode }) {
  const { isConnected } = useAccount();
  const chainId = useChainId();
  const { switchChain, isPending } = useSwitchChain();

  if (isConnected && chainId !== SUPPORTED_CHAIN_ID) {
    return (
      <div className="rounded-xl border border-amber-500/30 bg-amber-500/10 p-6 text-center">
        <p className="text-amber-200">Wrong network detected</p>
        <button
          onClick={() => switchChain({ chainId: SUPPORTED_CHAIN_ID })}
          disabled={isPending}
          className="mt-4 btn-primary"
        >
          {isPending ? 'Switching…' : 'Switch to Ethereum'}
        </button>
      </div>
    );
  }

  return <>{children}</>;
}

Handling disconnected, rejected, and failed states

Map every wallet error to a human-readable message. Users reject transactions constantly — treat it as normal, not exceptional.

// lib/wallet-errors.ts
export function getWalletErrorMessage(error: Error): string {
  const msg = error.message.toLowerCase();

  if (msg.includes('user rejected') || msg.includes('user denied')) {
    return 'Transaction cancelled. No funds were moved.';
  }
  if (msg.includes('insufficient funds')) {
    return 'Insufficient balance for this transaction and gas fees.';
  }
  if (msg.includes('network changed')) {
    return 'Network changed. Please refresh and try again.';
  }
  return 'Something went wrong. Please try again.';
}

Show disconnected states inline, not as full-page blocks. Portfolio panels should show a prompt to connect rather than empty broken layouts.

Balances and transaction status

Format all token amounts consistently. Show USD values alongside raw balances. For pending transactions, display a progress stepper: submitted → confirming → confirmed. Link to the block explorer on confirmation.

// components/TransactionStatus.tsx
import { useWaitForTransactionReceipt } from 'wagmi';

export function TransactionStatus({ hash }: { hash: `0x${string}` }) {
  const { isLoading, isSuccess, isError } = useWaitForTransactionReceipt({ hash });

  if (isLoading) return <span className="text-amber-400">Confirming…</span>;
  if (isSuccess) return (
    <a href={`https://etherscan.io/tx/${hash}`} className="text-emerald-400">
      Confirmed ✓
    </a>
  );
  if (isError) return <span className="text-red-400">Failed — try again</span>;
  return null;
}

Mobile wallet UX

Over half of wallet connections happen on mobile. Test with WalletConnect, MetaMask Mobile, and Rainbow. Avoid hover-only interactions. Use full-width tap targets (minimum 44px). Tables need horizontal scroll containers. Deep links must work when users return from their wallet app mid-transaction.

Persist connection state across tab switches. Mobile browsers suspend background tabs — use wagmi's auto-reconnect and show a brief "Reconnecting…" state on focus.

Security and trust indicators

Show the connected address with copy and explorer links. Display the active network name and icon. For transaction previews, list token amounts, recipient address, and estimated gas before the user signs. Never hide approval amounts — unlimited token approvals destroy trust.

Add subtle security cues: HTTPS badge, verified contract links, and data source attribution ("Prices from CoinGecko, updated 30s ago").

Practical examples for dashboards and DeFi

DeFi dashboards: gate personal portfolio data behind connection but show public protocol stats without it. Let users browse TVL charts and token lists before asking for wallet access.

DEX interfaces: show slippage, price impact, and minimum received before swap confirmation. Highlight when price impact exceeds 1%.

NFT marketplaces: show floor price and collection stats publicly. Require connection only for buy, sell, and portfolio views.

Common UX mistakes

Hiding the connect button after a failed attempt. Showing raw hex errors from the wallet. Not handling wallet disconnect mid-session. Forcing connection before showing any product value. Using tiny connect buttons on mobile. Missing loading states during the 2–3 second modal open delay. Not persisting preferred wallet across sessions.

FAQ

**Should I auto-connect on page load?** Yes, with wagmi's reconnect option enabled. Show a brief loading state while reconnecting, then render the connected or disconnected UI.

**How do I handle multiple wallets?** Let users pick from a list of detected connectors. Remember their last choice in localStorage via wagmi's storage config.

**What about read-only mode?** Let users paste an address to view public portfolio data without connecting. This is especially useful for analytics dashboards.

**How do I reduce connection friction?** Show product value first. Use social proof near the connect button. Offer email signup as an alternative for non-wallet features.

**Is WalletConnect still necessary in 2026?** Yes. It remains the standard bridge for mobile wallets and desktop-to-mobile signing flows.

Want to work together? I build Web3 dashboards and DeFi interfaces.