onchain-ui
Components

Chain Select

Network selector for wallet switching or filtering, with optional all-networks and unsupported-chain states.

Use ChainSelect wherever a user picks a network: an app header, a bridge form, or a token filter. It takes the chains you support as props and reports the id you picked — the wallet connection and the switch call stay in your app.

Loading...
chain-select.tsxShow code
import { ChainSelect } from "@/components/ui/chain-select"const chains = [  { id: 1, name: "Ethereum" },  { id: 8453, name: "Base" },  { id: 42161, name: "Arbitrum One" },  { id: 10, name: "Optimism" },  { id: 137, name: "Polygon" },]export function ChainSelectDemo() {  const [chainId, setChainId] = useState<number | null>(8453)  return (    <ChainSelect chains={chains} value={chainId} onSelect={setChainId} />  )}

Installation

npx shadcn add https://onchain-ui.dev/r/chain-select.json
Open in

Requirements

The menu composes the standard shadcn dropdown-menu. The install adds it if your app does not already have components/ui/dropdown-menu.tsx; if you do, your existing (possibly customized) menu is used as-is. NetworkLogo comes along for the chain marks.

Usage

import { ChainSelect } from "@/components/ui/chain-select"

<ChainSelect chains={chains} value={chainId} onSelect={setChainId} />

ChainSelect holds no connection state and performs no network calls. It renders the chains you hand it and calls onSelect — which makes it testable with plain data and keeps your wallet library out of the component.

For filters rather than wallet switching, add an all-networks item and keep null as the unfiltered state:

const [chainId, setChainId] = useState<number | null>(null)

<ChainSelect
  chains={chains}
  value={chainId}
  allLabel="All networks"
  menuLabel="Filter by network"
  onClear={() => setChainId(null)}
  onSelect={setChainId}
/>

Wiring with wagmi

Your configured chains are already the right list, so the wiring is a mapping:

"use client"

import { useChains, useConnection, useSwitchChain } from "wagmi"
import { ChainSelect } from "@/components/ui/chain-select"

export function ChainSwitcher() {
  const { chainId } = useConnection()
  const chains = useChains()
  const switchChain = useSwitchChain()

  return (
    <ChainSelect
      chains={chains.map((chain) => ({
        id: chain.id,
        name: chain.name,
        badge: chain.testnet ? "Testnet" : undefined,
      }))}
      value={chainId ?? null}
      pendingChainId={switchChain.isPending ? switchChain.variables?.chainId : null}
      onSelect={(id) => {
        const next = chains.find((chain) => chain.id === id)
        if (next) switchChain.mutate({ chainId: next.id })
      }}
    />
  )
}

onSelect hands you a number, but if you augment wagmi's Register type, switchChain expects your config's literal chain-id union — so passing the id straight through fails to compile. Looking the chain back up, as above, keeps it type-safe without a cast.

The snippet targets wagmi 3. On wagmi 2, read chainId from useAccount(), take the list from useSwitchChain().chains rather than useChains(), and call switchChain({ chainId }) instead of switchChain.mutate(...). The component itself is identical either way — it never imports wagmi.

Unsupported networks

A wallet can sit on any network, including ones your app was never configured for. When value is set but absent from chains, the trigger switches to a warning treatment and names the problem, and the menu still offers every supported chain so the user can get out.

Loading...
chain-select-unsupported.tsxShow code
import { ChainSelect } from "@/components/ui/chain-select"export function ChainSelectUnsupported() {  // The wallet is on a chain this app does not support.  const [chainId, setChainId] = useState<number | null>(5315)  return (    <ChainSelect chains={chains} value={chainId} onSelect={setChainId} />  )}

Without this state the chain id leaks into the UI as a bare number, which reads as a bug rather than as something the user can fix. Pass unsupportedLabel to reword it.

Examples

Disconnected

value={null} renders the placeholder with a dashed logo slot, so the control keeps its footprint before a wallet connects.

Loading...
chain-select-disconnected.tsxShow code
import { ChainSelect } from "@/components/ui/chain-select"export function ChainSelectDisconnected() {  const [chainId, setChainId] = useState<number | null>(null)  return (    <ChainSelect chains={chains} value={chainId} onSelect={setChainId} />  )}

Switching

pendingChainId marks the chain being switched to and locks the control while the wallet prompt is open.

Loading...
chain-select-pending.tsxShow code
import { ChainSelect } from "@/components/ui/chain-select"export function ChainSelectPending() {  return (    <ChainSelect      chains={chains}      value={8453}      pendingChainId={42161}      onSelect={switchChain}    />  )}

Sizes

Use showLabel={false} for a logo-only trigger in tight headers. The accessible name falls back to the network name.

Loading...
chain-select-sizes.tsxShow code
import { ChainSelect } from "@/components/ui/chain-select"export function ChainSelectSizes() {  return (    <div className="flex items-center gap-3">      <ChainSelect chains={chains} value={chainId} onSelect={setChainId} size="sm" />      <ChainSelect chains={chains} value={chainId} onSelect={setChainId} />      <ChainSelect chains={chains} value={chainId} onSelect={setChainId} showLabel={false} />    </div>  )}

Props

PropTypeDefaultDescription
chainsChainSelectOption[]-Selectable chains, usually the ones your app is configured for
valuenumber | null-Currently connected chain id
onSelect(chainId: number) => void-Called with the chain id the user picked
onClear() => void-Called when the optional all-networks item is picked
allLabelstring-Adds an item that clears the selected chain
pendingChainIdnumber | null-Chain being switched to. Blocks further selection
disabledbooleanfalseDisable the whole control
placeholderstring"Select network"Trigger label when value is null
unsupportedLabelstring"Unsupported network"Trigger label when value is missing from chains
menuLabelReactNode"Switch network"Heading above the menu items
showLabelbooleantrueShow the network name in the trigger
size"sm" | "default""default"Visual size
align"start" | "center" | "end""start"Menu alignment against the trigger
classNamestring-Applied to the trigger
contentClassNamestring-Applied to the menu

ChainSelectOption

PropTypeDefaultDescription
idnumber-EVM chain id
namestring | nullBuilt-in network nameDisplay name
logoSrcstring | nullBuilt-in chain logoNetwork image URL override
symbolstring | nullBuilt-in network symbolShort symbol for the logo fallback
disabledbooleanfalseRender but do not allow selection
badgeReactNode-Rendered after the name, e.g. a "Testnet" badge

On this page