Skip to main content

Building an ENS Profile Viewer

Learn how to build a complete ENS profile viewer that displays avatars, social links, and wallet information using the Web3 Identity API.

What We're Building

A React component that:

  • Resolves ENS names to addresses
  • Displays avatar images
  • Shows social media links
  • Handles loading and error states

Prerequisites

  • Node.js 18+
  • React 18+ (or Next.js)
  • Basic TypeScript knowledge

Step 1: Set Up Your Project

# Create a new React project
npx create-react-app ens-profile-viewer --template typescript
cd ens-profile-viewer

# Or with Next.js
npx create-next-app@latest ens-profile-viewer --typescript

Install Dependencies

npm install @web3identity/sdk
# Optional: for wallet connection
npm install wagmi viem @rainbow-me/rainbowkit

Step 2: Create the SDK Client

Create src/lib/web3identity.ts:

import { Web3IdentityClient } from '@web3identity/sdk';

// Initialize client (no API key needed for free tier)
export const client = new Web3IdentityClient({
// Optional: Add API key for higher rate limits
// apiKey: process.env.NEXT_PUBLIC_WEB3_IDENTITY_KEY
});

Step 3: Define Types

Create src/types/ens.ts:

export interface ENSProfile {
name: string;
address: string;
avatar: string | null;
records: {
twitter?: string;
github?: string;
discord?: string;
url?: string;
description?: string;
email?: string;
};
expiration?: string;
}

export interface ProfileState {
profile: ENSProfile | null;
loading: boolean;
error: string | null;
}

Step 4: Create the Profile Hook

Create src/hooks/useENSProfile.ts:

import { useState, useEffect, useCallback } from 'react';
import { client } from '../lib/web3identity';
import type { ENSProfile, ProfileState } from '../types/ens';

export function useENSProfile(ensName: string | null): ProfileState & {
refetch: () => void;
} {
const [state, setState] = useState<ProfileState>({
profile: null,
loading: false,
error: null,
});

const fetchProfile = useCallback(async () => {
if (!ensName) {
setState({ profile: null, loading: false, error: null });
return;
}

setState(prev => ({ ...prev, loading: true, error: null }));

try {
// Fetch full profile from API
const response = await fetch(
`https://api.web3identity.com/api/ens/${ensName}`
);

if (!response.ok) {
const error = await response.json();
throw new Error(error.message || 'Failed to fetch profile');
}

const data = await response.json();

const profile: ENSProfile = {
name: data.name,
address: data.address,
avatar: data.avatar,
records: {
twitter: data.records?.['com.twitter'] || data.records?.twitter,
github: data.records?.['com.github'] || data.records?.github,
discord: data.records?.['com.discord'],
url: data.records?.url,
description: data.records?.description,
email: data.records?.email,
},
expiration: data.expiration,
};

setState({ profile, loading: false, error: null });
} catch (error) {
setState({
profile: null,
loading: false,
error: error instanceof Error ? error.message : 'Unknown error',
});
}
}, [ensName]);

useEffect(() => {
fetchProfile();
}, [fetchProfile]);

return { ...state, refetch: fetchProfile };
}

Step 5: Build the Profile Component

Create src/components/ENSProfile.tsx:

import React from 'react';
import { useENSProfile } from '../hooks/useENSProfile';
import './ENSProfile.css';

interface ENSProfileProps {
ensName: string;
}

export function ENSProfile({ ensName }: ENSProfileProps) {
const { profile, loading, error } = useENSProfile(ensName);

if (loading) {
return (
<div className="ens-profile loading">
<div className="avatar-skeleton" />
<div className="text-skeleton" />
</div>
);
}

if (error) {
return (
<div className="ens-profile error">
<span className="error-icon">⚠️</span>
<p>{error}</p>
</div>
);
}

if (!profile) {
return null;
}

return (
<div className="ens-profile">
{/* Avatar */}
<div className="avatar-container">
{profile.avatar ? (
<img
src={profile.avatar}
alt={profile.name}
className="avatar"
onError={(e) => {
// Fallback to identicon
e.currentTarget.src = `https://api.web3identity.com/cdn/identicon/${profile.name}.svg`;
}}
/>
) : (
<img
src={`https://api.web3identity.com/cdn/identicon/${profile.name}.svg`}
alt={profile.name}
className="avatar"
/>
)}
</div>

{/* Name & Address */}
<div className="profile-info">
<h2 className="ens-name">{profile.name}</h2>
<p className="address">
{profile.address.slice(0, 6)}...{profile.address.slice(-4)}
<button
className="copy-btn"
onClick={() => navigator.clipboard.writeText(profile.address)}
title="Copy address"
>
📋
</button>
</p>
</div>

{/* Bio */}
{profile.records.description && (
<p className="bio">{profile.records.description}</p>
)}

{/* Social Links */}
<div className="social-links">
{profile.records.twitter && (
<a
href={`https://twitter.com/${profile.records.twitter}`}
target="_blank"
rel="noopener noreferrer"
className="social-link twitter"
>
<TwitterIcon /> @{profile.records.twitter}
</a>
)}

{profile.records.github && (
<a
href={`https://github.com/${profile.records.github}`}
target="_blank"
rel="noopener noreferrer"
className="social-link github"
>
<GithubIcon /> {profile.records.github}
</a>
)}

{profile.records.url && (
<a
href={profile.records.url}
target="_blank"
rel="noopener noreferrer"
className="social-link website"
>
🌐 Website
</a>
)}
</div>

{/* Expiration */}
{profile.expiration && (
<p className="expiration">
Expires: {new Date(profile.expiration).toLocaleDateString()}
</p>
)}
</div>
);
}

// Simple icon components
const TwitterIcon = () => (
<svg viewBox="0 0 24 24" width="16" height="16" fill="currentColor">
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
</svg>
);

const GithubIcon = () => (
<svg viewBox="0 0 24 24" width="16" height="16" fill="currentColor">
<path d="M12 0C5.374 0 0 5.373 0 12c0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23A11.509 11.509 0 0112 5.803c1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576C20.566 21.797 24 17.3 24 12c0-6.627-5.373-12-12-12z" />
</svg>
);

Step 6: Add Styles

Create src/components/ENSProfile.css:

.ens-profile {
max-width: 400px;
padding: 24px;
background: #ffffff;
border-radius: 16px;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}

.ens-profile.loading,
.ens-profile.error {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 200px;
}

.avatar-container {
display: flex;
justify-content: center;
margin-bottom: 16px;
}

.avatar {
width: 96px;
height: 96px;
border-radius: 50%;
object-fit: cover;
border: 3px solid #e5e7eb;
}

.profile-info {
text-align: center;
margin-bottom: 16px;
}

.ens-name {
font-size: 24px;
font-weight: 700;
color: #111827;
margin: 0 0 4px;
}

.address {
font-size: 14px;
color: #6b7280;
font-family: monospace;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}

.copy-btn {
background: none;
border: none;
cursor: pointer;
padding: 2px;
opacity: 0.7;
transition: opacity 0.2s;
}

.copy-btn:hover {
opacity: 1;
}

.bio {
text-align: center;
color: #374151;
font-size: 14px;
line-height: 1.5;
margin-bottom: 16px;
}

.social-links {
display: flex;
flex-direction: column;
gap: 8px;
}

.social-link {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
background: #f3f4f6;
border-radius: 8px;
text-decoration: none;
color: #374151;
font-size: 14px;
transition: background 0.2s;
}

.social-link:hover {
background: #e5e7eb;
}

.social-link.twitter { color: #1da1f2; }
.social-link.github { color: #333; }

.expiration {
text-align: center;
font-size: 12px;
color: #9ca3af;
margin-top: 16px;
}

/* Loading skeleton */
.avatar-skeleton {
width: 96px;
height: 96px;
border-radius: 50%;
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
}

.text-skeleton {
width: 150px;
height: 20px;
border-radius: 4px;
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
margin-top: 16px;
}

@keyframes shimmer {
0% { background-position: -200% 0; }
100% { background-position: 200% 0; }
}

Step 7: Create the Search Form

Create src/components/ENSSearch.tsx:

import React, { useState } from 'react';
import { ENSProfile } from './ENSProfile';

export function ENSSearch() {
const [input, setInput] = useState('');
const [searchName, setSearchName] = useState<string | null>(null);

const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
const name = input.trim();
if (name) {
// Add .eth if not present
const ensName = name.includes('.') ? name : `${name}.eth`;
setSearchName(ensName);
}
};

return (
<div className="ens-search">
<form onSubmit={handleSubmit}>
<input
type="text"
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Enter ENS name (e.g., vitalik.eth)"
className="search-input"
/>
<button type="submit" className="search-button">
Search
</button>
</form>

{searchName && <ENSProfile ensName={searchName} />}
</div>
);
}

Step 8: Use in Your App

import { ENSSearch } from './components/ENSSearch';

function App() {
return (
<div className="app">
<h1>ENS Profile Viewer</h1>
<ENSSearch />
</div>
);
}

export default App;

Advanced: Server-Side Rendering (Next.js)

For better SEO and performance, fetch data server-side:

// app/profile/[name]/page.tsx
import { ENSProfile } from '@/components/ENSProfile';

async function getProfile(name: string) {
const res = await fetch(
`https://api.web3identity.com/api/ens/${name}`,
{ next: { revalidate: 60 } } // Cache for 60 seconds
);

if (!res.ok) return null;
return res.json();
}

export default async function ProfilePage({
params
}: {
params: { name: string }
}) {
const profile = await getProfile(params.name);

if (!profile) {
return <div>Profile not found</div>;
}

return <ENSProfile data={profile} />;
}

Next Steps