dApp Frontend Development with Nuxt.js
The Vue ecosystem in Web3 loses to React in the number of ready-made libraries, but not in capabilities. The main challenge — most Web3 tooling is written React-first: wagmi, ConnectKit, RainbowKit only work with React. For Nuxt, you need either a different stack or wrappers. Let's look at what actually works in 2024-2025.
Web3 Stack for Nuxt 3
Wallet Connection
@wagmi/core + @web3modal/wagmi — wagmi core works without React, Vue wrapper @wagmi/vue appeared in wagmi v2. Web3Modal supports Vue through a separate package @web3modal/wagmi/vue. This is the most mature option:
// plugins/wagmi.client.ts
import { createWeb3Modal } from '@web3modal/wagmi/vue'
import { createConfig, http } from '@wagmi/vue'
import { mainnet, arbitrum } from '@wagmi/vue/chains'
import { walletConnect, injected } from '@wagmi/vue/connectors'
export default defineNuxtPlugin(() => {
const config = createConfig({
chains: [mainnet, arbitrum],
connectors: [
walletConnect({ projectId: useRuntimeConfig().public.wcProjectId }),
injected(),
],
transports: {
[mainnet.id]: http(),
[arbitrum.id]: http(),
},
})
createWeb3Modal({
wagmiConfig: config,
projectId: useRuntimeConfig().public.wcProjectId,
})
})
Plugin marked .client.ts is important — Web3 libraries are incompatible with SSR: they access window, localStorage, ethereum injector.
viem for reading blockchain data — works in any environment:
// composables/useContract.ts
import { createPublicClient, http, parseAbi } from 'viem'
import { mainnet } from 'viem/chains'
const client = createPublicClient({
chain: mainnet,
transport: http(),
})
export const useTokenBalance = (address: Ref<`0x${string}` | undefined>) => {
return useAsyncData(
`balance-${address.value}`,
() => address.value
? client.readContract({
address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
abi: parseAbi(['function balanceOf(address) view returns (uint256)']),
functionName: 'balanceOf',
args: [address.value],
})
: Promise.resolve(0n),
{ watch: [address] }
)
}
SSR Specifics
Nuxt 3 uses SSR by default, and this creates friction with Web3:
Hydration problem — window.ethereum exists only in the browser. Any component reading wallet state must be either in <ClientOnly> or check process.client:
<template>
<ClientOnly>
<WalletButton />
<template #fallback>
<button disabled>Loading...</button>
</template>
</ClientOnly>
</template>
Nuxt plugin with ssr: false — alternative to <ClientOnly>:
// nuxt.config.ts
export default defineNuxtConfig({
plugins: [
{ src: '~/plugins/wagmi.client.ts', mode: 'client' }
]
})
useNuxtApp().$wagmi — after initialization in plugin, wagmi config is available via Nuxt's provide/inject pattern.
Composables for Web3
The difference between React hooks and Vue composables in Web3 context: Vue reactivity through ref/computed works better for derived state that changes when account or network changes.
// composables/useWalletState.ts
export const useWalletState = () => {
const { address, isConnected, chain } = useAccount()
const shortAddress = computed(() =>
address.value
? `${address.value.slice(0, 6)}...${address.value.slice(-4)}`
: null
)
const isWrongNetwork = computed(() =>
isConnected.value && chain.value?.id !== mainnet.id
)
return { address, isConnected, shortAddress, isWrongNetwork }
}
Transactions and State
Managing transaction status is a typical pain point. wagmi v2 provides useWriteContract + useWaitForTransactionReceipt:
const { writeContract, data: hash } = useWriteContract()
const { isLoading: isConfirming, isSuccess } = useWaitForTransactionReceipt({ hash })
Pattern for UI: show three states — "sign in wallet", "transaction sent (hash)", "confirmed". Nuxt + Pinia conveniently keep this state globally for toast notifications.
Typical Project Stack
| Task | Library |
|---|---|
| Framework | Nuxt 3 (Vue 3 + Vite) |
| Wallet connect | @web3modal/wagmi + @wagmi/vue |
| On-chain reads | viem PublicClient |
| State | Pinia |
| Styling | Tailwind CSS 3 / UnoCSS |
| Data fetching | useAsyncData + TanStack Query |
Timeline for dApp frontend development on Nuxt — 1-2 weeks depending on complexity: basic wallet connection and contract reads take 3-4 days, full-featured dApp with multiple screens, transactions and error handling — 1.5 weeks.







