How to create a wallet connect button ウォレット接続ボタンの作り方
Connect MetaMask or any EIP-1193 wallet using window.ethereum only. Implements connect, disconnect, address display, and network display — no libraries required. MetaMask など EIP-1193 対応ウォレットを window.ethereum のみで接続。接続・切断・アドレス表示・ネットワーク表示をライブラリ不要で実装。
ライブデモ Live Demo
概要・用途・特徴Overview, Usage & Features
何ができるかWhat it does
Connect MetaMask or any EIP-1193 wallet using window.ethereum only. Implements connect, disconnect, address display, and network display — no libraries required.
MetaMask など EIP-1193 対応ウォレットを window.ethereum のみで接続。接続・切断・アドレス表示・ネットワーク表示をライブラリ不要で実装。
どこで使うかWhere to use
dApp interface, wallet integration, blockchain dashboard
NFTマーケットプレイス、DeFiアプリ、Web3ゲーム、DAO投票インターフェース
特徴Key features
Web3 wallet connect button supporting MetaMask and WalletConnect. Displays truncated wallet address on connection. Handles connect, disconnect, and account change events. Chain ID validation with user-friendly error messages.
MetaMaskとWalletConnectをサポートするWeb3ウォレット接続ボタン。接続時に短縮ウォレットアドレスを表示。接続・切断・アカウント変更イベントを処理。ユーザーフレンドリーなエラーメッセージ付きチェーンID検証。
調整可能パラメータ Adjustable Parameters
| Parameter | Default | Description |
|---|---|---|
CHAIN_NAMES | — | Mapping of chain ID (hex) to network name. Add or remove supported chains freely |
address truncation | — | <code>slice(0, 6) + '...' + slice(-4)</code> shows first 6 and last 4 chars. Adjust numbers to show more or fewer characters |
wallet_requestPermissions | — | Forces the account picker popup every time Connect is clicked. <code>eth_requestAccounts</code> returns the previous account silently on already-approved sites, so this is used instead |
button style | — | Customize the look via <code>.btn-connect</code> gradient, border-radius, and box-shadow |
実装コード Implementation Code
// react/B-001.jsx
import { useState, useEffect, useCallback } from 'react';
import './B-001.css';
const CHAIN_NAMES = {
'0x1': 'Ethereum', '0xaa36a7': 'Sepolia', '0x89': 'Polygon',
'0x2105': 'Base', '0xa': 'Optimism', '0xa4b1': 'Arbitrum', '0x38': 'BNB Chain',
};
export default function WalletConnectButton() {
const [status, setStatus] = useState('init'); // init | no-wallet | disconnected | connecting | connected
const [address, setAddress] = useState('');
const [network, setNetwork] = useState('');
const [copied, setCopied] = useState(false);
const showConnected = useCallback(async (addr) => {
const chainId = await window.ethereum.request({ method: 'eth_chainId' });
setAddress(addr);
setNetwork(CHAIN_NAMES[chainId] ?? `Chain ${parseInt(chainId, 16)}`);
setStatus('connected');
}, []);
useEffect(() => {
if (!window.ethereum) { setStatus('no-wallet'); return; }
setStatus('disconnected');
window.ethereum.request({ method: 'eth_accounts' })
.then(accs => { if (accs.length) showConnected(accs[0]); })
.catch(() => {});
const onAccounts = accs =>
accs.length ? showConnected(accs[0]) : setStatus('disconnected');
const onChain = () =>
window.ethereum.request({ method: 'eth_accounts' })
.then(accs => { if (accs.length) showConnected(accs[0]); })
.catch(() => {});
window.ethereum.on('accountsChanged', onAccounts);
window.ethereum.on('chainChanged', onChain);
return () => {
window.ethereum.removeListener('accountsChanged', onAccounts);
window.ethereum.removeListener('chainChanged', onChain);
};
}, [showConnected]);
async function connect() {
try {
setStatus('connecting');
await window.ethereum.request({
method: 'wallet_requestPermissions',
params: [{ eth_accounts: {} }],
});
const [addr] = await window.ethereum.request({ method: 'eth_accounts' });
await showConnected(addr);
} catch { setStatus('disconnected'); }
}
async function copyAddress() {
await navigator.clipboard.writeText(address);
setCopied(true);
setTimeout(() => setCopied(false), 1500);
}
const truncated = address ? address.slice(0, 6) + '...' + address.slice(-4) : '';
return (
<div className="wallet-demo">
{status === 'no-wallet' && (
<p className="no-wallet-msg">
No wallet detected.{' '}
<a href="https://metamask.io/" target="_blank" rel="noopener">Install MetaMask</a>
</p>
)}
{status === 'disconnected' && (
<button className="btn-connect" onClick={connect}>🦊 Connect Wallet</button>
)}
{status === 'connecting' && (
<div className="connecting-wrap">
<span className="w-spinner" /> Connecting...
</div>
)}
{status === 'connected' && (
<div className="wallet-card">
<div className="wallet-card-top">
<span className="connected-dot" />
<span className="network-name">{network}</span>
</div>
<div className="address-row">
<span className="address-text">{truncated}</span>
<button
className={`btn-copy${copied ? ' copied' : ''}`}
onClick={copyAddress}
>
{copied ? 'copied!' : 'copy'}
</button>
</div>
<button className="btn-disconnect" onClick={() => setStatus('disconnected')}>
Disconnect
</button>
</div>
)}
</div>
);
}
/* react/B-001.css */
.wallet-demo {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 200px;
padding: 40px 24px;
}
.btn-connect {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 13px 30px;
background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%);
color: #fff;
border: none;
border-radius: 12px;
font-size: 15px;
font-weight: 600;
cursor: pointer;
box-shadow: 0 4px 14px rgba(245, 158, 11, 0.35);
transition: filter 0.15s, transform 0.15s;
}
.btn-connect:hover { filter: brightness(1.08); transform: translateY(-2px); }
.connecting-wrap { display: flex; align-items: center; gap: 10px; color: #888; font-size: 14px; }
@keyframes spin { to { transform: rotate(360deg); } }
.w-spinner {
display: inline-block;
width: 18px; height: 18px;
border: 2px solid rgba(245, 158, 11, 0.25);
border-top-color: #f59e0b;
border-radius: 50%;
animation: spin 0.7s linear infinite;
}
.wallet-card {
background: #1c1c2a;
border: 1px solid #2a2a3a;
border-radius: 14px;
padding: 20px 24px;
display: flex;
flex-direction: column;
gap: 14px;
min-width: 270px;
}
.wallet-card-top { display: flex; align-items: center; gap: 8px; }
.connected-dot {
display: inline-block;
width: 8px; height: 8px;
border-radius: 50%;
background: #22c55e;
box-shadow: 0 0 6px rgba(34, 197, 94, 0.6);
}
.network-name { font-size: 13px; color: #888; font-weight: 500; }
.address-row { display: flex; align-items: center; gap: 8px; }
.address-text { font-family: 'Courier New', monospace; font-size: 15px; font-weight: 600; }
.btn-copy {
padding: 3px 10px;
background: transparent;
border: 1px solid #2a2a3a;
border-radius: 6px;
font-size: 12px; color: #888;
cursor: pointer;
transition: background 0.15s, color 0.15s;
}
.btn-copy:hover { background: #2a2a3a; color: #fff; }
.btn-copy.copied { color: #22c55e; border-color: #22c55e; }
.btn-disconnect {
background: transparent; border: none;
font-size: 13px; color: #888;
cursor: pointer; text-decoration: underline;
transition: color 0.15s;
}
.btn-disconnect:hover { color: #ef4444; }
.no-wallet-msg { font-size: 14px; color: #888; line-height: 1.7; }
.no-wallet-msg a { color: #fbbf24; }
import { useState, useEffect, useCallback } from 'react';
import './B-001.css';
const DC_KEY = 'devsnips-wallet-dc'; // localStorage flag: user explicitly disconnected
const CHAIN_NAMES = {
'0x1': 'Ethereum',
'0xaa36a7': 'Sepolia',
'0x89': 'Polygon',
'0x2105': 'Base',
'0xa': 'Optimism',
'0xa4b1': 'Arbitrum',
'0x38': 'BNB Chain',
};
export default function WalletConnectButton() {
// 'init' | 'no-wallet' | 'disconnected' | 'connecting' | 'connected'
const [status, setStatus] = useState('init');
const [address, setAddress] = useState('');
const [network, setNetwork] = useState('');
const [copied, setCopied] = useState(false);
const showConnected = useCallback(async (addr) => {
const chainId = await window.ethereum.request({ method: 'eth_chainId' });
setAddress(addr);
setNetwork(CHAIN_NAMES[chainId] ?? `Chain ${parseInt(chainId, 16)}`);
setStatus('connected');
}, []);
useEffect(() => {
if (!window.ethereum) { setStatus('no-wallet'); return; }
setStatus('disconnected');
// Silent check — skip if user explicitly disconnected
window.ethereum.request({ method: 'eth_accounts' })
.then(accs => { if (accs.length && !localStorage.getItem(DC_KEY)) showConnected(accs[0]); })
.catch(() => {});
const onAccounts = (accs) => {
if (accs.length && !localStorage.getItem(DC_KEY)) {
showConnected(accs[0]);
} else if (!accs.length) {
localStorage.setItem(DC_KEY, '1');
setStatus('disconnected');
}
};
const onChain = () => {
if (localStorage.getItem(DC_KEY)) return;
window.ethereum.request({ method: 'eth_accounts' })
.then(accs => { if (accs.length) showConnected(accs[0]); })
.catch(() => {});
};
window.ethereum.on('accountsChanged', onAccounts);
window.ethereum.on('chainChanged', onChain);
return () => {
window.ethereum.removeListener('accountsChanged', onAccounts);
window.ethereum.removeListener('chainChanged', onChain);
};
}, [showConnected]);
async function connect() {
try {
setStatus('connecting');
// wallet_requestPermissions forces MetaMask to show account picker every time
await window.ethereum.request({
method: 'wallet_requestPermissions',
params: [{ eth_accounts: {} }],
});
const [addr] = await window.ethereum.request({ method: 'eth_accounts' });
localStorage.removeItem(DC_KEY);
await showConnected(addr);
} catch {
setStatus('disconnected'); // user rejected
}
}
async function copyAddress() {
await navigator.clipboard.writeText(address);
setCopied(true);
setTimeout(() => setCopied(false), 1500);
}
const truncated = address ? address.slice(0, 6) + '...' + address.slice(-4) : '';
return (
<div className="wallet-demo">
{status === 'no-wallet' && (
<p className="no-wallet-msg">
No wallet detected.{' '}
<a href="https://metamask.io/" target="_blank" rel="noopener">Install MetaMask</a>
</p>
)}
{status === 'disconnected' && (
<button className="btn-connect" onClick={connect}>
🦊 Connect Wallet
</button>
)}
{status === 'connecting' && (
<div className="connecting-wrap">
<span className="w-spinner" /> Connecting...
</div>
)}
{status === 'connected' && (
<div className="wallet-card">
<div className="wallet-card-top">
<span className="connected-dot" />
<span className="network-name">{network}</span>
</div>
<div className="address-row">
<span className="address-text">{truncated}</span>
<button
className={`btn-copy${copied ? ' copied' : ''}`}
onClick={copyAddress}
>
{copied ? 'copied!' : 'copy'}
</button>
</div>
<button className="btn-disconnect" onClick={() => { localStorage.setItem(DC_KEY, '1'); setStatus('disconnected'); }}>
Disconnect
</button>
</div>
)}
</div>
);
}
.wallet-demo {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 200px;
padding: 40px 24px;
}
.btn-connect {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 13px 30px;
background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%);
color: #fff;
border: none;
border-radius: 12px;
font-size: 15px;
font-weight: 600;
cursor: pointer;
box-shadow: 0 4px 14px rgba(245, 158, 11, 0.35);
transition: filter 0.15s ease, transform 0.15s ease;
}
.btn-connect:hover {
filter: brightness(1.08);
transform: translateY(-2px);
}
.btn-connect:active {
transform: translateY(0);
filter: brightness(0.95);
}
.connecting-wrap {
display: flex;
align-items: center;
gap: 10px;
color: #888;
font-size: 14px;
}
@keyframes spin { to { transform: rotate(360deg); } }
.w-spinner {
display: inline-block;
width: 18px;
height: 18px;
border: 2px solid rgba(245, 158, 11, 0.25);
border-top-color: #f59e0b;
border-radius: 50%;
animation: spin 0.7s linear infinite;
}
.wallet-card {
background: #1c1c2a;
border: 1px solid #2a2a3a;
border-radius: 14px;
padding: 20px 24px;
display: flex;
flex-direction: column;
gap: 14px;
min-width: 270px;
}
.wallet-card-top {
display: flex;
align-items: center;
gap: 8px;
}
.connected-dot {
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
background: #22c55e;
box-shadow: 0 0 6px rgba(34, 197, 94, 0.6);
flex-shrink: 0;
}
.network-name {
font-size: 13px;
color: #888;
font-weight: 500;
}
.address-row {
display: flex;
align-items: center;
gap: 8px;
}
.address-text {
font-family: 'Courier New', monospace;
font-size: 15px;
font-weight: 600;
letter-spacing: 0.02em;
}
.btn-copy {
padding: 3px 10px;
background: transparent;
border: 1px solid #2a2a3a;
border-radius: 6px;
font-size: 12px;
color: #888;
cursor: pointer;
transition: background 0.15s, color 0.15s, border-color 0.15s;
}
.btn-copy:hover {
background: #2a2a3a;
color: #fff;
}
.btn-copy.copied {
color: #22c55e;
border-color: #22c55e;
}
.btn-disconnect {
align-self: flex-start;
background: transparent;
border: none;
font-size: 13px;
color: #888;
cursor: pointer;
text-decoration: underline;
padding: 0;
transition: color 0.15s;
}
.btn-disconnect:hover {
color: #ef4444;
}
.no-wallet-msg {
font-size: 14px;
color: #888;
line-height: 1.7;
text-align: center;
}
.no-wallet-msg a {
color: #fbbf24;
}
仕組みとカスタマイズHow It Works & Customization
仕組みHow it works
The button calls window.ethereum.request({ method: "eth_requestAccounts" }) on click. On success, it saves the returned address to state and displays a truncated version (0x1234...5678). Event listeners for accountsChanged and chainChanged update the UI reactively. Disconnecting clears the stored address and resets to the initial state.
ボタンはクリック時にwindow.ethereum.request({ method: "eth_requestAccounts" })を呼び出します。成功時に返されたアドレスを状態に保存して短縮バージョン(0x1234...5678)を表示。accountsChangedとchainChangedのイベントリスナーがUIをリアクティブに更新。切断時に保存されたアドレスをクリアして初期状態にリセット。
カスタマイズ方法Customization
Add support for multiple wallet providers by detecting which are installed. Show the connected chain name (Ethereum, Polygon, etc.) alongside the address. Add an avatar using the wallet address as a seed for a deterministic identicon.
インストール済みのものを検出することで複数のウォレットプロバイダーのサポートを追加。アドレスの横に接続されたチェーン名(Ethereum、Polygonなど)を表示。ウォレットアドレスをシードとして使用して決定論的なidenticonのアバターを追加。
注意点Caveats
Never store private keys or seed phrases in your application. Always validate chain ID before transactions — wrong network can cause fund loss. Provide a clear disconnect mechanism. Handle the case where window.ethereum is undefined (non-Web3 browser).
アプリケーションに秘密鍵やシードフレーズを保存しないでください。トランザクション前には常にチェーンIDを検証してください — 間違ったネットワークは資金損失を引き起こす可能性があります。明確な切断メカニズムを提供してください。window.ethereumが未定義(非Web3ブラウザ)の場合を処理してください。
よくある質問 Frequently Asked Questions
How to customize the wallet connect button? Wallet Connect Buttonをカスタマイズするには?
Modify the CSS custom properties and class styles defined in the code section. Key adjustable values include colors, sizes, durations, and spacing. See the Adjustable Parameters section for specific variables.
コードセクションで定義されているCSSカスタムプロパティとクラススタイルを変更してください。色、サイズ、時間、間隔が主な調整可能値です。具体的な変数は調整可能パラメータセクションを参照してください。
How to use the wallet connect button in React? ReactでWallet Connect Buttonを使うには?
Import the provided React component and its CSS file. The component accepts props for customization. Check the React code section for the full implementation and available props.
提供されているReactコンポーネントとCSSファイルをインポートしてください。コンポーネントのpropsでカスタマイズできます。完全な実装と利用可能なpropsはReactコードセクションを参照してください。
What are the performance implications of wallet connect button? Wallet Connect Buttonのパフォーマンスへの影響は?
This implementation uses CSS transforms and opacity for animations, which are GPU-accelerated. It's lightweight and doesn't cause layout thrashing. Consider using prefers-reduced-motion for accessibility.
この実装はCSSのtransformとopacityを使用しており、GPUアクセラレーションされます。軽量でレイアウトスラッシングを引き起こしません。アクセシビリティのためにprefers-reduced-motionの使用を検討してください。
AIへの指示テンプレート AI Prompt Template
以下をAIに貼り付けると実装を依頼できます。 Paste the following into your AI assistant to request implementation.