onchain-ui
uiaddress-displaysource: ok · hit
Address Display
Displays an EVM wallet address with copy-to-clipboard and an optional block explorer link.
npx shadcn add "https://onchain-ui.dev/r/address-display.json"free
Where it goes
kanban
Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.
Dependencies
Bundle weight · lighttoastssonner
iconslucide-react
Registry dependencies
@onchain-ui/sonner
Files3 · source cached 10/13/2026, 9:21:10 AM
registry/onchain-ui/address-display.tsx
"use client";
import { toast } from "sonner";
import { CopyIcon, ExternalLinkIcon } from "lucide-react";
import { cn } from "@/lib/utils";
import { truncateAddress } from "@/lib/onchain/format";
import { getExplorerUrl } from "@/lib/onchain/config";
import type { Address } from "viem";
import type { ReactNode } from "react";
export interface AddressDisplayProps {
/** EVM wallet address */
address: Address;
/** Display label. The copied value is always the underlying address. */
label?: ReactNode;
/** Applied to the root wrapper */
className?: string;
/** Applied to the address text or copy trigger */
addressClassName?: string;
/** Truncate the address. Default: true */
truncate?: boolean;
/** Number of characters to show at each end when truncated. Default: 4 */
truncateChars?: number;
/** Show copy-to-clipboard button. Default: true */
showCopy?: boolean;
/** Show block explorer link. Default: true */
showExplorer?: boolean;
/** EVM chain id used to pick a known block explorer. Default: 1 (Etherscan) */
chainId?: number | null;
/** Full explorer URL override e.g. "https://basescan.org/address/0x..." */
explorerUrl?: string;
/** Icon rendered before the address when copy is enabled */
copyIcon?: ReactNode;
/** Icon rendered for the explorer link */
explorerIcon?: ReactNode;
}
export function AddressDisplay({
address,
label,
className,
addressClassName,
truncate = true,
truncateChars = 4,
showCopy = true,
showExplorer = true,
chainId,
explorerUrl,
copyIcon = <CopyIcon className="size-3 shrink-0" />,
explorerIcon = <ExternalLinkIcon className="size-3 shrink-0" />,
}: AddressDisplayProps) {
const handleCopy = async () => {
try {
await navigator.clipboard.writeText(address);
toast.success("Address copied");
} catch {
toast.error("Could not copy address");
}
};
// No chain id means "assume mainnet", the documented default. A chain id we
// do not recognise is different: linking it to Etherscan would point at a
// page about a different chain's address space. Drop the link instead and
// let `explorerUrl` cover the long tail.
const explorerBase = getExplorerUrl(chainId ?? 1);
const href =
explorerUrl ??
(explorerBase ? `${explorerBase}/address/${address}` : null);
const displayLabel =
label ?? (truncate ? truncateAddress(address, truncateChars) : address);
const labelClassName = truncate ? "whitespace-nowrap" : "break-all";
return (
<div className={cn("flex min-w-0 items-center gap-1.5", className)}>
{showCopy ? (
<button
type="button"
onClick={handleCopy}
title="Copy address"
className={cn(
"flex min-w-0 items-center gap-1.5 font-mono text-xs text-muted-foreground",
"hover:text-foreground cursor-copy transition-colors duration-150",
addressClassName
)}
>
{copyIcon}
<span className={labelClassName}>{displayLabel}</span>
</button>
) : (
<span
className={cn(
"min-w-0 font-mono text-xs text-muted-foreground",
labelClassName,
addressClassName
)}
>
{displayLabel}
</span>
)}
{showExplorer && href && (
<a
href={href}
target="_blank"
rel="noopener noreferrer"
title="View on explorer"
className="text-muted-foreground hover:text-foreground transition-colors duration-150"
>
{explorerIcon}
</a>
)}
</div>
);
}
lib/onchain/format.ts
export function truncateAddress(address: string, chars = 4): string {
if (!address) return "";
return `${address.slice(0, chars + 2)}...${address.slice(-chars)}`;
}
export type NumericValue = number | string | null | undefined;
/** Parses numbers and numeric strings; anything else becomes null. */
export function parseNumericValue(value: NumericValue): number | null {
if (typeof value === "number") return Number.isFinite(value) ? value : null;
if (typeof value === "string" && value.trim() !== "") {
const parsed = Number(value);
return Number.isFinite(parsed) ? parsed : null;
}
return null;
}
lib/onchain/config.ts
import {
arbitrum,
base,
mainnet,
optimism,
polygon,
robinhood,
} from "viem/chains";
import type { AssetGatewayUrls, Chain, PublicClient } from "viem";
/** The small client surface identity resolution actually consumes. */
export type OnchainReadClient = Pick<
PublicClient,
"getEnsAddress" | "getEnsAvatar" | "getEnsName" | "readContract"
>;
/**
* The single place onchain-ui components look for app-wide settings.
*
* This file is yours: edit it once and every component follows. There is no
* provider and no context — components import these functions directly, so
* they work the same in a server component, a test, or a non-React consumer.
*
* Per-instance overrides always win. `<NetworkLogo name="…" />` and
* `<AddressDisplay explorerUrl="…" />` beat anything configured here.
*/
export interface OnchainUIConfig {
/**
* Chains your app supports. Supplies block explorer URLs and display names
* for networks that have no built-in entry below.
*
* Using wagmi? Point this at the config you already wrote:
*
* ```ts
* import { wagmiConfig } from "@/lib/onchain/wagmi";
* chains: wagmiConfig.chains,
* ```
*/
chains: readonly Chain[];
/**
* Returns a viem client for reads that are not tied to a rendered component
* — currently ENS and Basename resolution. Leave undefined to use the public
* endpoints below.
*
* Using wagmi? Share its transports instead of configuring RPC twice:
*
* ```ts
* import { getPublicClient } from "@wagmi/core";
* import { wagmiConfig } from "@/lib/onchain/wagmi";
* getClient: (chainId) => getPublicClient(wagmiConfig, { chainId }),
* ```
*/
getClient?: (chainId: number) => OnchainReadClient | undefined;
/**
* Public gateways used to turn IPFS and Arweave avatar records into URLs.
* Replace these with your own gateway in production when reliability or
* rate limits matter.
*/
avatarGatewayUrls?: AssetGatewayUrls;
}
export const onchainConfig: OnchainUIConfig = {
chains: [mainnet, base, arbitrum, optimism, polygon, robinhood],
avatarGatewayUrls: {
ipfs: "https://gateway.pinata.cloud",
arweave: "https://arweave.net",
},
};
/**
* Curated network names and symbols.
*
* These are deliberately not derived from the viem chain: `chain.name` gives
* "OP Mainnet" where a badge wants "Optimism", and `nativeCurrency.symbol`
* gives "ETH" for Base, Optimism, and Arbitrum alike, which is useless as a
* network mark. Chains absent here fall back to the configured chain's name.
*/
const knownNetworks: Partial<
Record<number, { name: string; symbol: string }>
> = {
1: { name: "Ethereum", symbol: "ETH" },
10: { name: "Optimism", symbol: "OP" },
137: { name: "Polygon", symbol: "POL" },
4663: { name: "Robinhood Chain", symbol: "RH" },
8453: { name: "Base", symbol: "BASE" },
42161: { name: "Arbitrum", symbol: "ARB" },
};
function findChain(chainId?: number | null) {
if (chainId == null) return null;
return onchainConfig.chains.find((chain) => chain.id === chainId) ?? null;
}
export interface NetworkMeta {
name: string | null;
symbol: string | null;
}
/** Display name and symbol for a chain, or nulls when it is unrecognised. */
export function getNetworkMeta(chainId?: number | null): NetworkMeta {
if (chainId == null) return { name: null, symbol: null };
const known = knownNetworks[chainId];
if (known) return known;
return { name: findChain(chainId)?.name ?? null, symbol: null };
}
/**
* Block explorer base URL for a chain, or null when we have none.
*
* Derived from the configured chains rather than a second hardcoded map —
* viem's chain objects already carry `blockExplorers`. Null is a real answer:
* an address on an unrecognised chain does not exist in mainnet's address
* space, so callers render no link rather than a misleading one.
*/
export function getExplorerUrl(chainId?: number | null): string | null {
const chain = findChain(chainId);
return chain?.blockExplorers?.default.url ?? null;
}
/** A read client for `chainId`, if the app configured one. */
export function getConfiguredClient(
chainId: number
): OnchainReadClient | undefined {
return onchainConfig.getClient?.(chainId);
}
/** App-wide gateways for ENS and Basename avatar assets, if configured. */
export function getConfiguredAvatarGatewayUrls(): AssetGatewayUrls | undefined {
return onchainConfig.avatarGatewayUrls;
}