onchain-ui
Recipes

Wallet Connection

Wire onchain-ui components to a connected wallet with wagmi.

Most onchain-ui components are presentation-only — they take addresses, amounts, and metadata as props and don't care where those come from. For wallet connections on EVM chains we use wagmi: typed hooks for accounts, balances, and chains, built on the same viem primitives the components already use.

onchain-ui is EVM-first: components accept viem Address values and the resolvers understand ENS and Basenames. We don't have a recommendation for Solana or other ecosystems yet.

Setup

npm install wagmi viem @tanstack/react-query

These examples target wagmi 3. If you are on v2, the differences that matter here: useConnection was useAccount, useConnectors() was useConnect().connectors, useChains() was useSwitchChain().chains, and the mutation hooks were called directly (connect({ connector })) rather than through mutate. Only connect-wallet and use-available-connectors touch any of this — every other component in the registry is wagmi-agnostic and works on either.

Wagmi is configured once and provided at the root. It uses TanStack Query for caching under the hood, so the same QueryClient serves your own data fetching too (see Data Fetching).

// app/providers.tsx
"use client"

import { WagmiProvider, createConfig, http } from "wagmi"
import { base, mainnet } from "wagmi/chains"
import { injected } from "wagmi/connectors"
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"

const config = createConfig({
  chains: [mainnet, base],
  connectors: [injected()],
  transports: {
    [mainnet.id]: http("https://your-rpc-provider.example/mainnet"),
    [base.id]: http("https://your-rpc-provider.example/base"),
  },
})

const queryClient = new QueryClient()

export function Providers({ children }: { children: React.ReactNode }) {
  return (
    <WagmiProvider config={config}>
      <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
    </WagmiProvider>
  )
}

Connect surface

Install Connect Wallet and the connect, identity, and disconnect flow is done:

npx shadcn add https://onchain-ui.dev/r/connect-wallet.json
"use client"

import { ConnectWallet } from "@/components/ui/connect-wallet"

export function Header() {
  return <ConnectWallet />
}

It probes for wallets the browser actually announces, connects to the one the user picks, resolves the address to a Basename or ENS name, and offers disconnect. Scope is injected() plus EIP-6963 — browser extension wallets, no SDKs, no project id, nothing to sign up for.

If you want the flow but not the layout, the same install gives you the parts separately: ConnectorList and WalletButton take plain data, and useAvailableConnectors returns the probed wagmi connectors. Build your own arrangement from those rather than overriding ours.

When you need more than injected wallets

QR codes, mobile deep links, email or passkey sign-in, and smart accounts all need a connector with its own SDK, which wagmi 3 ships as optional peer dependencies you install yourself — see wagmi's connector list. Those connectors appear in useConnectors() like any other, so ConnectWallet lists them with no change on our side.

Full modal kits (Reown AppKit, RainbowKit, Privy, Dynamic) bring their own connect UI and their own opinions. They work on the same wagmi config, but check their peer ranges first — several are still pinned to wagmi 2.

Account chip

Prefer a hand-rolled chip? useConnection feeds AddressIdentity directly — it resolves the connected wallet's Basename or ENS name and avatar, and falls back to the truncated address.

npx shadcn add button card https://onchain-ui.dev/r/address-identity.json
"use client"

import { useConnection, useConnect, useDisconnect } from "wagmi"
import { injected } from "wagmi/connectors"
import { Button } from "@/components/ui/button"
import { Card } from "@/components/ui/card"
import { AddressIdentity } from "@/components/ui/address-identity"

export function AccountChip() {
  const { address, isConnected } = useConnection()
  const connect = useConnect()
  const disconnect = useDisconnect()

  if (!isConnected || !address) {
    return (
      <Button onClick={() => connect.mutate({ connector: injected() })}>
        Connect wallet
      </Button>
    )
  }

  return (
    <Card size="sm" className="flex-row items-center gap-3 px-3">
      <AddressIdentity address={address} />
      <Button variant="ghost" size="sm" onClick={() => disconnect.mutate()}>
        Disconnect
      </Button>
    </Card>
  )
}

Native balance

useBalance returns a bigint value; format it with viem's formatUnits and hand it to TokenBalance.

"use client"

import { useBalance, useConnection } from "wagmi"
import { formatUnits } from "viem"
import { TokenBalance } from "@/components/ui/token-balance"

export function GasBalance() {
  const { address } = useConnection()
  const { data: balance } = useBalance({ address })

  return (
    <TokenBalance
      amount={balance ? formatUnits(balance.value, balance.decimals) : null}
      symbol={balance?.symbol}
    />
  )
}

Reuse wagmi's clients for identity resolution

AddressIdentity accepts viem clients through resolverOptions. Instead of configuring RPC endpoints twice, reuse the clients wagmi already created from your transports config:

"use client"

import { usePublicClient } from "wagmi"
import { base, mainnet } from "wagmi/chains"
import { AddressIdentity } from "@/components/ui/address-identity"
import type { Address } from "viem"

export function ResolvedIdentity({ address }: { address: Address }) {
  const mainnetClient = usePublicClient({ chainId: mainnet.id })
  const baseClient = usePublicClient({ chainId: base.id })

  return (
    <AddressIdentity
      address={address}
      resolverOptions={{ mainnetClient, baseClient }}
    />
  )
}

Lookup results are cached per client, so every component using the wagmi clients shares one cache — see Address Identity → Caching.

On this page