Using <ClientOnly> in Nuxt 3 to Safely Access Browser APIs
Learn how to wrap browser‑dependent components with <ClientOnly> in Nuxt 3 to avoid SSR errors, see the mechanism in action, and avoid common pitfalls.
10 Aug 2026, 14:39 UTC

Why solves the "window is not defined" error in Nuxt 3
When a Nuxt 3 page is rendered on the server, the initial HTML is generated without a browser environment. Any code that tries to access window, document or localStorage throws a reference error and breaks the SSR step.
How the mechanism works
Nuxt 3 uses Nitro to render the page on the server, then sends the HTML to the browser where Vue hydrates the markup. The <ClientOnly> wrapper tells Vue to skip rendering the wrapped component during the server pass and mount it only after hydration on the client.
// components/BrowserWidget.vue
Local storage value: {{ stored }}
If you import BrowserWidget directly in a page, the server will try to execute localStorage.getItem and fail.
// pages/index.vue
Welcome
During the server render, <ClientOnly> renders nothing (or a fallback if you provide one). After the HTML is sent to the browser, Vue hydrates the page and then mounts BrowserWidget, allowing it to safely read localStorage.
Limits and common mistakes
- Failing to wrap the component leads to
window is not definederrors during SSR, which appear as 500 responses or blank pages. - Providing a fallback inside
<ClientOnly>(e.g.,<ClientOnly><BrowserWidget /><template #fallback>Loading…</template></ClientOnly>) is optional but improves perceived performance. - Using
process.clientchecks insetup instead of<ClientOnly>still runs the component on the server, so the error persists. - If you set
ssr: falseinnuxt.config.ts, the whole app becomes a SPA and<ClientOnly>is unnecessary, but you lose the SEO benefits of server‑rendered HTML. - Hydration mismatches can appear if the server‑rendered HTML differs from what the client expects after the component mounts. Keep the wrapper’s outer HTML identical on both passes (e.g., do not conditionally render different root elements based on
process.client).
Verification steps
- Open the page with JavaScript disabled (browser setting or extension) – you should still see the surrounding markup (the
<h1>) and any fallback you provided. - Enable JavaScript and open the browser console; there should be no “window is not defined” errors.
- Inspect the Network tab; the initial HTML request contains the server‑rendered markup but no separate XHR for the widget’s data.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.