Skip to main content

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โ€‹