| name | web3-frontend-ux-reviewer |
| description | Review and improve Web3 dApp frontends for better user experience. Covers wallet integration patterns, transaction flows, gas optimization, Web3 UI component libraries, and blockchain-specific UX best practices. Use when: (1) Reviewing dApp interfaces, (2) Building Web3 frontends, (3) Improving Web3 UX, (4) Evaluating Web3 projects, (5) Auditing blockchain applications. |
Web3 Frontend & UX Reviewer
Comprehensive guide for reviewing and improving Web3 dApp frontends with focus on user experience, wallet integration, and blockchain-specific patterns.
Quick Reference
Wallet Connection Patterns
| Pattern | Best For | Complexity |
|---|
| Modal Selection | Multiple wallets | Low |
| In-page Button | Single wallet | Low |
| Animated Connector | UX-focused | Medium |
| Hardware Wallet Flow | Security-focused | High |
Transaction States
| State | User Feedback | Next Action |
|---|
| Idle | Show button | User can initiate |
| Pending | Spinner + message | Wait for confirm |
| Success | Checkmark + hash | View explorer |
| Error | Alert + retry | Fix and retry |
| Rejected | Clear message | Request again |
Critical UX Metrics
- Wallet Connection Time: < 3 seconds
- Transaction Confirm: < 30 seconds (L2) / < 5 min (L1)
- Gas Display: Show USD value
- Error Recovery: Clear paths forward
Wallet Integration Standards
Connection Flow Best Practices
interface WalletConnectionState {
status: 'idle' | 'connecting' | 'connected' | 'disconnected' | 'error';
address: string | null;
chainId: number | null;
balance: string | null;
error: string | null;
}
const getConnectionUI = (state: WalletConnectionState) => {
switch (state.status) {
case 'idle':
return {
button: 'Connect Wallet',
icon: null,
message: null
};
case 'connecting':
return {
button: 'Connecting...',
icon: <Spinner />,
message: 'Please confirm in your wallet'
};
case 'connected':
return {
button: truncateAddress(state.address),
icon: <WalletIcon chainId={state.chainId} />,
message: `${formatBalance(state.balance)} ETH`
};
case 'error':
return {
button: 'Retry',
icon: <AlertIcon />,
message: state.error
};
}
};
Multi-Wallet Support
const SUPPORTED_WALLETS = [
{
name: 'MetaMask',
icon: '/icons/metamask.svg',
downloadUrl: 'https://metamask.io/download/',
deepLink: 'metamask://connect',
injectedProvider: 'isMetaMask',
},
{
name: 'WalletConnect',
icon: '/icons/walletconnect.svg',
qrCode: true,
bridgeUrl: 'https://bridge.walletconnect.org',
},
{
name: 'Coinbase Wallet',
icon: '/icons/coinbase.svg',
downloadUrl: 'https://www.coinbase.com/wallet',
injectedProvider: 'isCoinbaseWallet',
},
{
name: 'Rainbow',
icon: '/icons/rainbow.svg',
downloadUrl: 'https://rainbow.me/download',
deepLink: 'rainbow://connect',
},
];
Chain Switching
const CHAIN_CONFIG = {
1: { name: 'Ethereum', type: 'L1', explorer: 'etherscan.io' },
137: { name: 'Polygon', type: 'L2', explorer: 'polygonscan.com' },
42161: { name: 'Arbitrum', type: 'L2', explorer: 'arbiscan.io' },
10: { name: 'Optimism', type: 'L2', explorer: 'optimistic.etherscan.io' },
8453: { name: 'Base', type: 'L2', explorer: 'basescan.org' },
};
const handleChainSwitch = async (targetChainId: number) => {
try {
await window.ethereum.request({
method: 'wallet_switchEthereumChain',
params: [{ chainId: `0x${targetChainId.toString(16)}` }],
});
} catch (error) {
if (error.code === 4902) {
await addChain(targetChainId);
}
}
};
Transaction Flow UX
Transaction Preview
interface TransactionPreview {
title: string;
description: string;
changes: {
label: string;
before: string;
after: string;
type: 'increase' | 'decrease' | 'neutral';
}[];
gas: {
estimate: string;
usdValue: string;
};
risks: {
type: 'approval' | 'unlimited_spend' | 'oracle';
message: string;
mitigation?: string;
}[];
nonce: number;
maxFee: string;
}
const TransactionPreviewModal = ({ tx, onConfirm, onCancel }: Props) => (
<Modal>
<ModalHeader>
<Icon name="warning" />
<Title>Review Transaction</Title>
</ModalHeader>
<ModalBody>
<RiskAlert risks={tx.risks} />
<TransactionDetails tx={tx} />
<GasEstimate gas={tx.gas} />
</ModalBody>
<ModalFooter>
<Button variant="secondary" onClick={onCancel}>Cancel</Button>
<Button variant="primary" onClick={onConfirm}>Confirm</Button>
</ModalFooter>
</Modal>
);
Approval Token Risks
| Risk Level | Pattern | User Warning |
|---|
| 🔴 High | Unlimited ERC-20 approval | "This gives unlimited access to your tokens" |
| 🟡 Medium | Large approval amount | "Consider approving only what you need" |
| 🟢 Low | Limited approval | "Safe approval amount" |
Gas Estimation Display
const GasDisplay = ({ gasEstimate, speed = 'average' }) => {
const speeds = {
slow: { multiplier: 0.8, label: 'Slow' },
average: { multiplier: 1.0, label: 'Average' },
fast: { multiplier: 1.2, label: 'Fast' },
};
const adjustedGas = gasEstimate * speeds[speed].multiplier;
const usdCost = adjustedGas * gasPrice * ethPrice;
return (
<div className="gas-display">
<label>Network Fee</label>
<Value>
{formatGwei(adjustedGas)} Gwei
<span className="usd">(${usdCost.toFixed(2)})</span>
</Value>
<SpeedSelector selected={speed} onChange={setSpeed} />
</div>
);
};
Web3 UX Best Practices
Onboarding Flow
User Journey
├─ Landing Page
│ └─ Clear value proposition (not "Connect Wallet" hero)
│
├─ Wallet Connect (optional at this stage)
│ └─ Show preview of functionality without connection
│
├─ Tutorial/Guide (first-time users)
│ ├─ Wallet setup guide
│ ├─ Network explanation
│ └─ Gas concept explainer
│
└─ Main Interface
└─ Progressive disclosure of features
Error Handling Patterns
const Web3ErrorHandler = ({ error, onRetry, onHelp }: Props) => {
const getErrorConfig = (error: Error) => {
const configs = {
'INSUFFICIENT_FUNDS': {
icon: '💸',
title: 'Insufficient Balance',
message: 'You need more ETH for gas',
action: 'Buy ETH',
link: '/buy-eth',
},
'USER_REJECTED': {
icon: '✋',
title: 'Transaction Rejected',
message: 'You rejected the transaction in your wallet',
action: 'Try Again',
},
'NETWORK_ERROR': {
icon: '🌐',
title: 'Network Mismatch',
message: 'Please switch to the correct network',
action: 'Switch Network',
},
'SLIPPAGE_EXCEEDED': {
icon: '📉',
title: 'High Slippage',
message: 'Price changed significantly',
action: 'Retry with higher slippage',
},
};
return configs[error.code] || {
icon: '⚠️',
title: 'Transaction Failed',
message: error.message,
action: 'Retry',
};
};
const config = getErrorConfig(error);
return <ErrorCard config={config} onAction={onRetry} />;
};
Loading States
const Web3Loading = ({ status, message }) => {
const loadingStates = {
'wallet_connecting': {
spinner: true,
message: 'Connecting to wallet...',
tips: [
'Tip: Check your wallet popup',
'Tip: Make sure MetaMask is unlocked',
],
},
'chain_switching': {
spinner: true,
message: 'Switching networks...',
tips: ['This may take a few seconds'],
},
'transaction_signing': {
spinner: true,
message: 'Sign in your wallet',
tips: [
'Your transaction is waiting for signature',
'Do not close this window',
],
},
'transaction_mining': {
spinner: true,
message: 'Transaction mining...',
progress: true,
link: 'View on Explorer',
},
};
return (
<LoadingScreen
{...loadingStates[status]}
message={message}
/>
);
};
dApp Interface Patterns
Dashboard Layout
Header
├─ Logo + Navigation
├─ Network Selector
├─ Wallet Button (address + balance)
└─ Notifications Bell
Main Content
├─ Stats Cards (TVL, Volume, APY)
├─ Quick Actions
│ ├─ Swap
│ ├─ Stake
│ └─ Bridge
│
├─ Main Feature Area
│ └─ Cards/Tables with data
│
└─ Recent Activity
Footer
├─ Social Links
├─ Documentation
├─ Support
└─ Language Selector
Data Display Standards
| Data Type | Format | Example |
|---|
| Addresses | Truncated + copy | 0x1234...5678 |
| Amounts | Full + symbol | 1,234.56 USDC |
| Prices | USD format | $1,234.56 |
| Percentages | 2 decimal places | 12.34% |
| Timestamps | Relative + tooltip | "2 hours ago" |
| Transactions | Hash + explorer | 0xabc...def |
Mobile Considerations
const MobileWalletSheet = ({ isOpen, onClose, children }) => (
<Sheet
isOpen={isOpen}
position="bottom"
onClose={onClose}
sizes={['50%', '75%', '100%']}
>
<Sheet.Container>
<Sheet.Header>
<DragHandle />
<WalletBalance />
</Sheet.Header>
<Sheet.Body>
{children}
</Sheet.Body>
</Sheet.Container>
</Sheet>
);
const MobileNetworkSelector = () => (
<RadioGroup>
<NetworkOption chainId={1} icon="ethereum" />
<NetworkOption chainId={137} icon="polygon" />
<NetworkOption chainId={42161} icon="arbitrum" />
</RadioGroup>
);
Web3 UI Component Libraries
React Component Libraries
| Library | GitHub Stars | Features |
|---|
| web3uikit | 1.4K | Lightweight, 20+ components |
| web3-ui (Developer-DAO) | 785 | Hooks-based, Wagmi integration |
| mochi-ui | 1.8K | Beautiful, accessible, themable |
| on-chain-ui | - | On-chain components |
| OpenZeppelin ui-builder | - | Form builder for contracts |
Library Comparison
web3uikit:
├─ Pros: Lightweight, easy to use
├─ Cons: Limited customization
└─ Best for: Quick prototypes
mochi-ui:
├─ Pros: Beautiful default styles, accessible
├─ Cons: Larger bundle size
└─ Best for: Production dApps
web3-ui (Developer-DAO):
├─ Pros: Wagmi integration, hooks
├─ Cons: Still in development
└─ Best for: Wagmi-based projects
OpenZeppelin ui-builder:
├─ Pros: No-code form generation
├─ Cons: Limited to contract interaction
└─ Best for: Contract interfaces
Web3 UX Checklist
Pre-Launch Review
Security Checks
Accessibility
Performance
Common dApp Patterns
Swap Interface
interface SwapInterface {
fromToken: Token;
toToken: Token;
amount: string;
slippage: number;
route: Route[];
priceImpact: number;
gasCost: string;
minimumReceived: string;
}
const SwapReviewScreen = ({ swap }: Props) => (
<ReviewCard>
<DirectionArrow from={swap.fromToken} to={swap.toToken} />
<RateDisplay
rate={swap.rate}
priceImpact={swap.priceImpact}
impactLevel={getImpactLevel(swap.priceImpact)}
/>
<RouteDisplay route={swap.route} />
<GasDisplay gas={swap.gasCost} />
<SlippageSelector value={swap.slippage} onChange={setSlippage} />
<Warnings warnings={getSwapWarnings(swap)} />
</ReviewCard>
);
NFT Display
const NFTGallery = ({ tokens, columns = 4 }) => (
<div className={`grid grid-cols-${columns} gap-4`}>
{tokens.map((token) => (
<NFTCard
image={token.image}
name={token.name}
collection={token.collection}
rarity={token.rarity}
onSelect={() => handleSelect(token)}
/>
))}
</div>
);
const NFTCard = ({ image, name, rarity, onClick }) => (
<Card onClick={onClick}>
<Image src={image} alt={name} loading="lazy" />
<CardBody>
<Name>{name}</Name>
<RarityBadge level={rarity}>{rarity}</RarityBadge>
</CardBody>
</Card>
);
Resources
Documentation
Design Resources
UX Research