> For the complete documentation index, see [llms.txt](https://docs.dothype.io/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.dothype.io/integrating-.hype/build-with-dothype/custom-connect-button.md).

# Custom Connect Button

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` domains
* Uses the `DotHypeResolver.name(address)` function
* Falls 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

<details>

<summary>Full Code</summary>

```tsx
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,
  };
}
```

</details>

### Step 3. CSS Styles

<details>

<summary>Styles.css</summary>

```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;
}
```

</details>

### Step 4. Usage Example

<details>

<summary>TSX</summary>

```tsx
import { CustomConnectButton } from './CustomConnectButton';
import { useChainId } from 'wagmi';

function App() {
  const chainId = useChainId();
  
  return (
    <div>
      <CustomConnectButton chainId={chainId} />
    </div>
  );
}
```

</details>

### :warning: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.
