Build an ENS Profile Card
Create a beautiful, reusable profile card that displays ENS data. This tutorial takes you from zero to a production-ready component.
What You'll Buildโ
A compact profile card that:
- Shows avatar, name, and address
- Displays social links (Twitter, GitHub, website)
- Handles loading, error, and empty states
- Works in React, Vue, or vanilla JavaScript
Time: ~30 minutes
Difficulty: Beginner
Live Demoโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ โญโโโโโโโโฎ โ
โ โ ๐ผ๏ธ โ vitalik.eth โ
โ โ Avatarโ 0xd8dA...96045 โ
โ โฐโโโโโโโโฏ โ
โ โ
โ Co-founder of Ethereum โ
โ โ
โ ๐ฆ @VitalikButerin ๐ฆ vbuterin โ
โ ๐ vitalik.eth.limo โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Step 1: Understand the APIโ
First, let's see what data we get from the ENS API:
curl https://api.web3identity.com/api/ens/vitalik.eth
Response:
{
"name": "vitalik.eth",
"address": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
"avatar": "https://metadata.ens.domains/mainnet/avatar/vitalik.eth",
"records": {
"twitter": "VitalikButerin",
"github": "vbuterin",
"url": "https://vitalik.eth.limo",
"description": "ethereum.org"
},
"expiration": "2032-05-04T00:00:00Z"
}
Step 2: Vanilla JavaScript Versionโ
Let's start with plain JavaScriptโno frameworks needed:
HTML Structureโ
<!DOCTYPE html>
<html>
<head>
<title>ENS Profile Card</title>
<style>
.ens-card {
max-width: 320px;
padding: 20px;
border-radius: 16px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
box-shadow: 0 10px 40px rgba(0,0,0,0.2);
}
.ens-card__header {
display: flex;
align-items: center;
gap: 16px;
margin-bottom: 16px;
}
.ens-card__avatar {
width: 64px;
height: 64px;
border-radius: 50%;
border: 3px solid rgba(255,255,255,0.3);
object-fit: cover;
}
.ens-card__name {
font-size: 20px;
font-weight: 700;
margin: 0;
}
.ens-card__address {
font-size: 12px;
opacity: 0.8;
font-family: monospace;
}
.ens-card__bio {
font-size: 14px;
line-height: 1.5;
margin-bottom: 16px;
opacity: 0.9;
}
.ens-card__links {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.ens-card__link {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 6px 12px;
background: rgba(255,255,255,0.2);
border-radius: 20px;
color: white;
text-decoration: none;
font-size: 13px;
transition: background 0.2s;
}
.ens-card__link:hover {
background: rgba(255,255,255,0.3);
}
.ens-card--loading {
min-height: 180px;
display: flex;
align-items: center;
justify-content: center;
}
.ens-card--error {
background: linear-gradient(135deg, #ff6b6b 0%, #ee5a5a 100%);
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.loading-text {
animation: pulse 1.5s infinite;
}
</style>
</head>
<body>
<div id="profile-card"></div>
<script>
const BASE_URL = 'https://api.web3identity.com';
async function fetchENSProfile(name) {
const response = await fetch(`${BASE_URL}/api/ens/${name}`);
if (!response.ok) {
throw new Error(response.status === 404
? 'ENS name not found'
: 'Failed to fetch profile');
}
return response.json();
}
function truncateAddress(address) {
return `${address.slice(0, 6)}...${address.slice(-4)}`;
}
function renderProfileCard(profile) {
const links = [];
if (profile.records?.twitter) {
links.push({
icon: '๐ฆ',
label: `@${profile.records.twitter}`,
url: `https://twitter.com/${profile.records.twitter}`
});
}
if (profile.records?.github) {
links.push({
icon: '๐ฆ',
label: profile.records.github,
url: `https://github.com/${profile.records.github}`
});
}
if (profile.records?.url) {
links.push({
icon: '๐',
label: 'Website',
url: profile.records.url
});
}
return `
<div class="ens-card">
<div class="ens-card__header">
<img
class="ens-card__avatar"
src="${profile.avatar || `${BASE_URL}/cdn/identicon/${profile.name}.svg`}"
alt="${profile.name}"
onerror="this.src='${BASE_URL}/cdn/identicon/${profile.name}.svg'"
/>
<div>
<h2 class="ens-card__name">${profile.name}</h2>
<div class="ens-card__address" title="${profile.address}">
${truncateAddress(profile.address)}
</div>
</div>
</div>
${profile.records?.description
? `<p class="ens-card__bio">${profile.records.description}</p>`
: ''}
${links.length > 0 ? `
<div class="ens-card__links">
${links.map(link => `
<a class="ens-card__link" href="${link.url}" target="_blank" rel="noopener">
${link.icon} ${link.label}
</a>
`).join('')}
</div>
` : ''}
</div>
`;
}
function renderLoading() {
return `
<div class="ens-card ens-card--loading">
<span class="loading-text">Loading profile...</span>
</div>
`;
}
function renderError(message) {
return `
<div class="ens-card ens-card--error">
<p>โ ๏ธ ${message}</p>
</div>
`;
}
// Initialize
async function init(ensName) {
const container = document.getElementById('profile-card');
container.innerHTML = renderLoading();
try {
const profile = await fetchENSProfile(ensName);
container.innerHTML = renderProfileCard(profile);
} catch (error) {
container.innerHTML = renderError(error.message);
}
}
// Load vitalik.eth on page load
init('vitalik.eth');
</script>
</body>
</html>
Step 3: React Componentโ
Here's a production-ready React component:
Installationโ
npm install react react-dom
ENSProfileCard.tsxโ
import React, { useState, useEffect } from 'react';
const BASE_URL = 'https://api.web3identity.com';
interface ENSProfile {
name: string;
address: string;
avatar: string | null;
records: {
twitter?: string;
github?: string;
url?: string;
description?: string;
email?: string;
};
}
interface ENSProfileCardProps {
ensName: string;
className?: string;
onLoad?: (profile: ENSProfile) => void;
onError?: (error: Error) => void;
}
export function ENSProfileCard({
ensName,
className = '',
onLoad,
onError
}: ENSProfileCardProps) {
const [profile, setProfile] = useState<ENSProfile | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
async function fetchProfile() {
setLoading(true);
setError(null);
try {
const response = await fetch(`${BASE_URL}/api/ens/${ensName}`);
if (!response.ok) {
throw new Error(
response.status === 404
? 'ENS name not found'
: 'Failed to fetch profile'
);
}
const data = await response.json();
setProfile(data);
onLoad?.(data);
} catch (err) {
const message = err instanceof Error ? err.message : 'Unknown error';
setError(message);
onError?.(err as Error);
} finally {
setLoading(false);
}
}
if (ensName) {
fetchProfile();
}
}, [ensName, onLoad, onError]);
const truncateAddress = (addr: string) =>
`${addr.slice(0, 6)}...${addr.slice(-4)}`;
if (loading) {
return (
<div className={`ens-card ens-card--loading ${className}`}>
<div className="ens-card__spinner" />
<span>Loading...</span>
</div>
);
}
if (error) {
return (
<div className={`ens-card ens-card--error ${className}`}>
<span>โ ๏ธ {error}</span>
</div>
);
}
if (!profile) return null;
const socialLinks = [
profile.records?.twitter && {
icon: '๐ฆ',
label: `@${profile.records.twitter}`,
url: `https://twitter.com/${profile.records.twitter}`
},
profile.records?.github && {
icon: '๐ฆ',
label: profile.records.github,
url: `https://github.com/${profile.records.github}`
},
profile.records?.url && {
icon: '๐',
label: 'Website',
url: profile.records.url
}
].filter(Boolean);
return (
<div className={`ens-card ${className}`}>
<div className="ens-card__header">
<img
className="ens-card__avatar"
src={profile.avatar || `${BASE_URL}/cdn/identicon/${profile.name}.svg`}
alt={profile.name}
onError={(e) => {
e.currentTarget.src = `${BASE_URL}/cdn/identicon/${profile.name}.svg`;
}}
/>
<div className="ens-card__info">
<h2 className="ens-card__name">{profile.name}</h2>
<button
className="ens-card__address"
onClick={() => navigator.clipboard.writeText(profile.address)}
title="Click to copy"
>
{truncateAddress(profile.address)} ๐
</button>
</div>
</div>
{profile.records?.description && (
<p className="ens-card__bio">{profile.records.description}</p>
)}
{socialLinks.length > 0 && (
<div className="ens-card__links">
{socialLinks.map((link: any) => (
<a
key={link.url}
className="ens-card__link"
href={link.url}
target="_blank"
rel="noopener noreferrer"
>
{link.icon} {link.label}
</a>
))}
</div>
)}
</div>
);
}
Usageโ
import { ENSProfileCard } from './ENSProfileCard';
function App() {
return (
<div className="app">
<ENSProfileCard
ensName="vitalik.eth"
onLoad={(profile) => console.log('Loaded:', profile.name)}
onError={(err) => console.error('Error:', err)}
/>
{/* Multiple cards */}
<ENSProfileCard ensName="nick.eth" />
<ENSProfileCard ensName="brantly.eth" />
</div>
);
}
Step 4: Python Script Versionโ
For backend use or CLI tools:
#!/usr/bin/env python3
"""
ENS Profile Card Generator
Generates HTML profile cards from ENS names
"""
import requests
import html
from typing import Optional
BASE_URL = "https://api.web3identity.com"
def fetch_ens_profile(name: str) -> dict:
"""Fetch ENS profile from Web3 Identity API."""
response = requests.get(f"{BASE_URL}/api/ens/{name}")
response.raise_for_status()
return response.json()
def truncate_address(address: str) -> str:
"""Truncate Ethereum address for display."""
return f"{address[:6]}...{address[-4:]}"
def generate_profile_card(name: str) -> str:
"""Generate HTML profile card for an ENS name."""
try:
profile = fetch_ens_profile(name)
except requests.exceptions.HTTPError as e:
if e.response.status_code == 404:
return f'<div class="ens-card error">ENS name "{name}" not found</div>'
raise
# Build social links
links = []
records = profile.get("records", {})
if records.get("twitter"):
links.append({
"icon": "๐ฆ",
"label": f"@{records['twitter']}",
"url": f"https://twitter.com/{records['twitter']}"
})
if records.get("github"):
links.append({
"icon": "๐ฆ",
"label": records["github"],
"url": f"https://github.com/{records['github']}"
})
if records.get("url"):
links.append({
"icon": "๐",
"label": "Website",
"url": records["url"]
})
# Generate HTML
avatar_url = profile.get("avatar") or f"{BASE_URL}/cdn/identicon/{name}.svg"
address = truncate_address(profile["address"])
bio = html.escape(records.get("description", ""))
links_html = ""
if links:
links_html = '<div class="links">' + "".join(
f'<a href="{link["url"]}" target="_blank">{link["icon"]} {link["label"]}</a>'
for link in links
) + '</div>'
return f'''
<div class="ens-card">
<div class="header">
<img src="{avatar_url}" alt="{name}" class="avatar" />
<div class="info">
<h2>{name}</h2>
<span class="address">{address}</span>
</div>
</div>
{f'<p class="bio">{bio}</p>' if bio else ''}
{links_html}
</div>
'''
def main():
"""CLI usage example."""
import sys
names = sys.argv[1:] if len(sys.argv) > 1 else ["vitalik.eth", "nick.eth"]
print("""
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>ENS Profile Cards</title>
<style>
body { font-family: sans-serif; padding: 20px; background: #f0f0f0; }
.ens-card {
max-width: 300px; padding: 20px; margin: 10px;
background: linear-gradient(135deg, #667eea, #764ba2);
border-radius: 16px; color: white; display: inline-block;
}
.ens-card .header { display: flex; align-items: center; gap: 12px; }
.ens-card .avatar { width: 48px; height: 48px; border-radius: 50%; }
.ens-card h2 { margin: 0; font-size: 18px; }
.ens-card .address { font-size: 11px; opacity: 0.8; font-family: monospace; }
.ens-card .bio { font-size: 13px; margin: 12px 0; opacity: 0.9; }
.ens-card .links { display: flex; flex-wrap: wrap; gap: 8px; }
.ens-card .links a {
padding: 4px 10px; background: rgba(255,255,255,0.2);
border-radius: 12px; color: white; text-decoration: none; font-size: 12px;
}
.ens-card.error { background: #ff6b6b; }
</style>
</head>
<body>
""")
for name in names:
print(generate_profile_card(name))
print("</body></html>")
if __name__ == "__main__":
main()
Run it:
python ens_profile_card.py vitalik.eth nick.eth brantly.eth > profiles.html
Step 5: cURL + jq One-Linerโ
Quick profile card data extraction:
#!/bin/bash
# ens-card.sh - Extract profile card data from ENS
ENS_NAME="${1:-vitalik.eth}"
BASE_URL="https://api.web3identity.com"
curl -s "$BASE_URL/api/ens/$ENS_NAME" | jq '{
name: .name,
address: (.address[:6] + "..." + .address[-4:]),
avatar: (.avatar // "No avatar"),
bio: (.records.description // "No bio"),
twitter: (.records.twitter // null),
github: (.records.github // null),
website: (.records.url // null)
}'
Output:
{
"name": "vitalik.eth",
"address": "0xd8dA...6045",
"avatar": "https://metadata.ens.domains/mainnet/avatar/vitalik.eth",
"bio": "ethereum.org",
"twitter": "VitalikButerin",
"github": "vbuterin",
"website": "https://vitalik.eth.limo"
}
Bonus: Multiple Cards Gridโ
Display multiple profiles in a responsive grid:
const profiles = ['vitalik.eth', 'nick.eth', 'brantly.eth', 'dwr.eth'];
function ProfileGrid() {
return (
<div style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fill, minmax(280px, 1fr))',
gap: '20px',
padding: '20px'
}}>
{profiles.map(name => (
<ENSProfileCard key={name} ensName={name} />
))}
</div>
);
}
Complete Example Repositoryโ
Download the full example:
git clone https://github.com/ATV-eth/ens-profile-card-example
cd ens-profile-card-example
npm install
npm run dev
Next Stepsโ
- Add x402 payment support for premium features
- Implement SIWE authentication to show "verified" badge
- Add Farcaster data for social context