Custom Connect Button
Display .hype Names Instead of Wallet Addresses
This component enhances your RainbowKit wallet connection UX by displaying the user's primary .hype
name (if set) instead of a truncated address. It works seamlessly across HyperEVM mainnet and testnet, supports loading states, and falls back to address when no name is set.
Step 1. Overview & Purpose
This custom component:
Replaces wallet addresses with
.hype
domainsUses the
DotHypeResolver.name(address)
functionFalls back to the address if no primary name is set
Detects network automatically via chain ID
Supports full styling customization
⚠️ Important: Replace the placeholder contract addresses in the code with your actual deployed resolver addresses (see
/info
page for details).
Step 2. CustomConnectButton.tsx
Full Code
import { useState, useEffect, useCallback } from "react";
import { ConnectButton } from "@rainbow-me/rainbowkit";
import { usePublicClient } from "wagmi";
import { Address } from "viem";
import { getCurrentNetworkAddresses } from "@/contracts/addresses";
import { DOT_HYPE_RESOLVER_ABI } from "@/contracts/abis";
/**
* Custom ConnectButton that shows primary domain name instead of truncated address
* when a primary domain is set for the connected wallet
*/
export function CustomConnectButton() {
return (
<ConnectButton.Custom>
{({
account,
chain,
openAccountModal,
openChainModal,
openConnectModal,
authenticationStatus,
mounted,
}) => {
// Note: If your app doesn't use authentication, you
// can remove all 'authenticationStatus' checks
const ready = mounted && authenticationStatus !== "loading";
const connected =
ready &&
account &&
chain &&
(!authenticationStatus || authenticationStatus === "authenticated");
return (
<div
{...(!ready && {
"aria-hidden": true,
style: {
opacity: 0,
pointerEvents: "none",
userSelect: "none",
},
})}
>
{(() => {
if (!connected) {
return (
<button
onClick={openConnectModal}
type="button"
className="bg-gradient-to-r from-hype-primary to-hype-secondary text-white px-6 py-3 rounded-lg font-medium hover:from-hype-secondary hover:to-hype-primary transition-all duration-200 shadow-lg hover:shadow-xl"
>
Connect Wallet
</button>
);
}
if (chain.unsupported) {
return (
<button
onClick={openChainModal}
type="button"
className="bg-red-500 text-white px-4 py-2 rounded-lg font-medium hover:bg-red-600 transition-colors"
>
Wrong network
</button>
);
}
return (
<div className="flex items-center gap-3">
<button
onClick={openChainModal}
className="flex items-center gap-2 bg-gray-100 dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-gray-600 px-3 py-3 rounded-lg transition-colors"
type="button"
>
{chain.hasIcon && (
<div
style={{
background: chain.iconBackground,
width: 20,
height: 20,
borderRadius: 999,
overflow: "hidden",
marginRight: 4,
}}
>
{chain.iconUrl && (
<img
alt={chain.name ?? "Chain icon"}
src={chain.iconUrl}
style={{ width: 20, height: 20 }}
/>
)}
</div>
)}
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">
{chain.name}
</span>
</button>
<AccountButton
account={account}
openAccountModal={openAccountModal}
/>
</div>
);
})()}
</div>
);
}}
</ConnectButton.Custom>
);
}
/**
* Account button component that shows primary domain or truncated address
*/
interface AccountButtonProps {
account: {
address: string;
displayBalance?: string;
};
openAccountModal: () => void;
}
function AccountButton({ account, openAccountModal }: AccountButtonProps) {
const { primaryDomain, isLoading: primaryDomainLoading } = usePrimaryDomain(
account?.address as Address
);
const { avatar, isLoading: avatarLoading } = useUserAvatar(
account?.address as Address
);
const displayText =
primaryDomain ||
`${account.address.slice(0, 6)}...${account.address.slice(-4)}`;
const showBalance = account.displayBalance
? ` (${account.displayBalance})`
: "";
const isLoading = primaryDomainLoading || avatarLoading;
return (
<button
onClick={openAccountModal}
type="button"
className="flex items-center gap-3 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700 px-4 py-3 rounded-lg border border-gray-300 dark:border-gray-600 transition-colors shadow-sm"
>
{/* Avatar or Status Indicator */}
<div className="flex-shrink-0">
{isLoading ? (
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-hype-primary"></div>
) : avatar ? (
<AvatarImage src={avatar} />
) : (
<div className="w-8 h-8 bg-gradient-to-br from-hype-primary to-hype-secondary rounded-full flex items-center justify-center">
<div className="w-2 h-2 bg-white rounded-full"></div>
</div>
)}
</div>
{/* Account Info */}
<div className="flex items-center gap-2">
<span className="text-sm font-medium text-gray-900 dark:text-white">
{displayText}
</span>
{showBalance && (
<span className="text-sm text-gray-600 dark:text-gray-400">
{showBalance}
</span>
)}
</div>
</button>
);
}
/**
* Avatar image component with fallback
*/
interface AvatarImageProps {
src: string;
}
function AvatarImage({ src }: AvatarImageProps) {
const [hasError, setHasError] = useState(false);
if (hasError) {
return (
<div className="w-8 h-8 bg-gradient-to-br from-hype-primary to-hype-secondary rounded-full flex items-center justify-center">
<div className="w-2 h-2 bg-white rounded-full"></div>
</div>
);
}
return (
<img
src={src}
alt="User avatar"
className="w-8 h-8 rounded-full object-cover border-2 border-green-500"
onError={() => setHasError(true)}
/>
);
}
// Hook implementations used in the CustomConnectButton above
/**
* Hook to fetch the primary domain name for an address using the getName function
* @param address - The Hyperliquid address to get the primary domain for
*/
export function usePrimaryDomain(address?: Address): {
primaryDomain: string | null;
isLoading: boolean;
error: string | null;
refetch: () => Promise<void>;
} {
const [primaryDomain, setPrimaryDomain] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const publicClient = usePublicClient();
const addresses = getCurrentNetworkAddresses();
const resolverAddress = addresses.DOT_HYPE_RESOLVER as Address;
const fetchPrimaryDomain = useCallback(async () => {
if (!publicClient || !address) {
setPrimaryDomain(null);
setIsLoading(false);
setError(null);
return;
}
setIsLoading(true);
setError(null);
try {
// Call getName function on the resolver
const domainName = await publicClient.readContract({
address: resolverAddress,
abi: DOT_HYPE_RESOLVER_ABI,
functionName: "getName",
args: [address],
});
// Check if we got a valid domain name
if (
domainName &&
typeof domainName === "string" &&
domainName.trim() !== ""
) {
setPrimaryDomain(domainName);
} else {
setPrimaryDomain(null);
}
} catch (err) {
console.error("Error fetching primary domain:", err);
// Handle specific error cases
let errorMessage = "Failed to fetch primary domain";
if (err instanceof Error) {
if (err.message.includes("execution reverted")) {
errorMessage = "No primary domain set for this address";
} else if (err.message.includes("OpcodeNotFound")) {
errorMessage = "Resolver contract does not support getName function";
} else {
errorMessage = err.message;
}
}
setError(errorMessage);
setPrimaryDomain(null);
} finally {
setIsLoading(false);
}
}, [address, publicClient, resolverAddress]);
useEffect(() => {
fetchPrimaryDomain();
}, [fetchPrimaryDomain]);
return {
primaryDomain,
isLoading,
error,
refetch: fetchPrimaryDomain,
};
}
/**
* Hook to fetch the user's avatar from their primary domain's text records
* Uses the resolver's getValue function to get the avatar directly by address
*/
export function useUserAvatar(address?: Address): {
avatar: string | null;
isLoading: boolean;
error: string | null;
} {
const [avatar, setAvatar] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const publicClient = usePublicClient();
useEffect(() => {
if (!address || !publicClient) {
setAvatar(null);
setIsLoading(false);
setError(null);
return;
}
const fetchAvatar = async () => {
setIsLoading(true);
setError(null);
try {
const { DOT_HYPE_RESOLVER } = getCurrentNetworkAddresses();
// Use getValue to get the avatar text record directly by address
const avatarValue = (await publicClient.readContract({
address: DOT_HYPE_RESOLVER as `0x${string}`,
abi: DOT_HYPE_RESOLVER_ABI,
functionName: "getValue",
args: [address, "avatar"],
})) as string;
// Only set avatar if it's a valid non-empty string
if (avatarValue && avatarValue.trim() !== "") {
setAvatar(avatarValue.trim());
} else {
setAvatar(null);
}
} catch (err) {
console.warn("Error fetching user avatar:", err);
setAvatar(null);
setError(err instanceof Error ? err.message : "Failed to fetch avatar");
} finally {
setIsLoading(false);
}
};
fetchAvatar();
}, [address, publicClient]);
return {
avatar,
isLoading,
error,
};
}
Step 3. CSS Styles
Styles.css
/* CSS Styles for Components */
/* Connect Button Styles */
.connect-button {
background: linear-gradient(135deg, #6366f1, #8b5cf6);
color: white;
padding: 8px 24px;
border-radius: 8px;
font-weight: 500;
border: none;
cursor: pointer;
transition: all 0.2s ease;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.connect-button:hover {
background: linear-gradient(135deg, #8b5cf6, #6366f1);
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.15);
}
.wrong-network-button {
background: #ef4444;
color: white;
padding: 8px 16px;
border-radius: 8px;
font-weight: 500;
border: none;
cursor: pointer;
transition: background-color 0.2s ease;
}
.wrong-network-button:hover {
background: #dc2626;
}
.connected-container {
display: flex;
align-items: center;
gap: 12px;
}
.chain-button {
display: flex;
align-items: center;
gap: 8px;
background: rgba(156, 163, 175, 0.1);
padding: 8px 12px;
border-radius: 8px;
border: none;
cursor: pointer;
transition: background-color 0.2s ease;
font-size: 14px;
}
.chain-button:hover {
background: rgba(156, 163, 175, 0.2);
}
.chain-icon {
width: 20px;
height: 20px;
border-radius: 50%;
}
.account-button {
display: flex;
align-items: center;
gap: 8px;
background: white;
border: 1px solid #d1d5db;
padding: 8px 16px;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s ease;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.account-button:hover {
background: #f9fafb;
}
.account-info {
display: flex;
align-items: center;
gap: 8px;
}
.status-indicator {
width: 8px;
height: 8px;
background: #10b981;
border-radius: 50%;
}
.loading-spinner {
width: 16px;
height: 16px;
border: 2px solid #e5e7eb;
border-top: 2px solid #6366f1;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* Address Input Styles */
.address-input-container {
width: 100%;
}
.input-label {
display: block;
font-size: 14px;
font-weight: 500;
color: #374151;
margin-bottom: 8px;
}
.input-wrapper {
position: relative;
}
.address-input {
width: 100%;
padding: 12px 40px 12px 12px;
border: 1px solid #d1d5db;
border-radius: 8px;
font-size: 14px;
transition: all 0.2s ease;
background: white;
}
.address-input:focus {
outline: none;
border-color: #6366f1;
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);
}
.address-input.loading {
border-color: #3b82f6;
}
.address-input.valid {
border-color: #10b981;
}
.address-input.error {
border-color: #ef4444;
}
.input-status-icon {
position: absolute;
right: 12px;
top: 50%;
transform: translateY(-50%);
font-size: 16px;
}
.status-message {
margin-top: 4px;
font-size: 14px;
display: flex;
align-items: center;
gap: 4px;
}
.status-message.loading {
color: #3b82f6;
}
.status-message.success {
color: #10b981;
}
.status-message.error {
color: #ef4444;
}
.resolution-display {
margin-top: 8px;
padding: 12px;
border-radius: 6px;
font-size: 14px;
}
.resolution-display.domain {
background: rgba(59, 130, 246, 0.1);
border: 1px solid rgba(59, 130, 246, 0.2);
}
.resolution-display.address {
background: rgba(16, 185, 129, 0.1);
border: 1px solid rgba(16, 185, 129, 0.2);
}
.resolution-content {
display: flex;
align-items: center;
gap: 8px;
}
.avatar-thumbnail {
width: 24px;
height: 24px;
border-radius: 4px;
border: 1px solid rgba(0, 0, 0, 0.1);
object-fit: cover;
flex-shrink: 0;
}
.resolution-value {
font-family: monospace;
word-break: break-all;
color: #1f2937;
flex: 1;
}
.help-text {
margin-top: 4px;
font-size: 12px;
color: #6b7280;
}
/* Dark mode styles */
:global(.dark) .chain-button {
background: rgba(75, 85, 99, 0.5);
color: #f3f4f6;
}
:global(.dark) .account-button {
background: #1f2937;
border-color: #374151;
color: white;
}
:global(.dark) .account-button:hover {
background: #111827;
}
:global(.dark) .address-input {
background: #1f2937;
border-color: #374151;
color: white;
}
:global(.dark) .input-label {
color: #d1d5db;
}
:global(.dark) .help-text {
color: #9ca3af;
}
:global(.dark) .resolution-value {
color: #f3f4f6;
}
Step 4. Usage Example
TSX
import { CustomConnectButton } from './CustomConnectButton';
import { useChainId } from 'wagmi';
function App() {
const chainId = useChainId();
return (
<div>
<CustomConnectButton chainId={chainId} />
</div>
);
}
⚠️Pro Tip
The component automatically detects the network based on the chain Id prop. Chain ID 999 is used for HyperEVM mainnet, while any other value defaults to testnet.
Last updated