This guide explains practical ways to connect a Web3-enabled website or dApp to MetaMask using Ethers.js and Web3.js. I'll show clear, testable examples for front-end JavaScript, React, and local development (Ganache and Remix). I believe hands-on examples are the fastest way to learn. I've been using these patterns daily while building DeFi integrations and testing smart contracts.
What you'll get: working code snippets, event handling tips, and security reminders (like token approvals and gas settings). And yes — we'll cover both Ethers.js v5 and v6 so you can use whichever your project requires.
There are three common ways a website connects to a user's software wallet:
| Method | Pros | Cons |
|---|---|---|
| Injected provider (window.ethereum) | Fast, user-friendly, supports signing transactions | Requires wallet installed and unlocked |
| WalletConnect | Mobile-friendly, works without extension | Extra UX step, session handling |
| RPC fallback | Good for read-only calls (balances) | Cannot sign user transactions |
If you want to learn how to add the extension or mobile app to your environment first, see the setup guide: /install-metamask-extension and /install-metamask-mobile-app.
Ethers.js is a popular library for interacting with Ethereum and EVM-compatible chains. Below are minimal patterns for requesting account access and getting a signer.
// Ethers v5
import { ethers } from 'ethers';
async function connectEthersV5() {
if (!window.ethereum) throw new Error('No injected provider');
const provider = new ethers.providers.Web3Provider(window.ethereum);
// Request account access
await provider.send('eth_requestAccounts', []);
const signer = provider.getSigner();
const address = await signer.getAddress();
console.log('Connected address', address);
return { provider, signer };
}
// Ethers v6
import { ethers } from 'ethers';
async function connectEthersV6() {
if (!window.ethereum) throw new Error('No injected provider');
const provider = new ethers.BrowserProvider(window.ethereum);
// Request account access
await provider.send('eth_requestAccounts', []);
const signer = await provider.getSigner();
const address = await signer.getAddress();
console.log('Connected address', address);
return { provider, signer };
}
These examples cover the common keyword patterns: ethers js connect metamask, ethers.js connect metamask, and connect metamask javascript.
If your project uses Web3.js, the flow is similar but uses the Web3 provider wrapper.
import Web3 from 'web3';
async function connectWeb3js() {
if (!window.ethereum) throw new Error('No injected provider');
await window.ethereum.request({ method: 'eth_requestAccounts' });
const web3 = new Web3(window.ethereum);
const accounts = await web3.eth.getAccounts();
console.log('Account', accounts[0]);
return { web3, accounts };
}
This addresses connect metamask web3js and web3 connect metamask queries.
React apps often need to manage provider state and clean up listeners. Here’s a minimal pattern using hooks.
import React, { useState, useEffect } from 'react';
import { ethers } from 'ethers';
function useMetaMask() {
const [address, setAddress] = useState(null);
const [provider, setProvider] = useState(null);
useEffect(() => {
if (!window.ethereum) return;
const p = new ethers.providers.Web3Provider(window.ethereum);
setProvider(p);
const handleAccounts = (accounts) => setAddress(accounts[0] || null);
window.ethereum.on('accountsChanged', handleAccounts);
return () => {
window.ethereum.removeListener('accountsChanged', handleAccounts);
};
}, []);
return { provider, address };
}
That pattern helps with connect metamask react implementations (see also /connect-web3-react for library integrations).
Want to test locally? Two common workflows:
Ganache: run a local RPC (often http://127.0.0.1:7545). Add a custom RPC in the wallet with the correct chainId (check your Ganache settings). Then import one of Ganache's private keys for signing, or enable unlocked accounts.
Remix: set the environment to "Injected Provider" so Remix routes requests to the wallet. See /connect-remix and /connect-ganache-local for step-by-step setup.
But take care: if you import a private key into your daily-use software wallet, only do so in a disposable account.
The wallet emits events you must handle: accountsChanged, chainChanged, and sometimes disconnect. Use them to keep UI state consistent.
window.ethereum.on('accountsChanged', (accounts) => { /* update UI */ });
window.ethereum.on('chainChanged', (chainId) => { window.location.reload(); });
What if the user rejects the request? Catch the thrown error and show a clear prompt. (A modal that explains why you need the address often helps.)
Personally, I once approved an unlimited allowance for a token while testing and had to revoke it later — a small mistake that was an expensive lesson. Keep wallets for daily use and hardware devices for large holdings.
Account abstraction and smart contract wallets (e.g., session keys and batched transactions) can improve UX for gasless or staged actions. Learn more: /smart-contract-wallets-aa and /account-abstraction.
For UI-level problems with connect buttons and session persistence, check /connect-button-troubleshoot and /troubleshooting-dapp-connections.
Who this is good for:
Who should look elsewhere:
Connecting a website to the injected provider is straightforward once you understand provider patterns and event handling. Test everything on a testnet or local node before launching to mainnet. If you're building a React app, check out /connect-web3-react and the deeper developer docs at /developer-integration and /metamask-api-connect.
Try the example code above in a small sandbox project. What I've found is that a simple connect button and clear error handling cover 90% of user issues. But keep iterating on UX and be strict about approvals and gas prompts.
Further reading and tools: /connect-remix, /connect-ganache-local, and the token approvals guide /token-approvals-revoke.
Happy building — and test with a disposable account first.