
Fix Nuxt hydration mismatch errors fast. Learn why hydration breaks, the most common causes, and practical, production-safe solutions to stop layout shifts and crashes.

Get notified when we release new tutorials, lessons, and other expert Nuxt content.
You see this error in your console:
[Vue warn]: Hydration node mismatch...
Or maybe this one:
Hydration children mismatch on <div>
Your app looks broken: buttons don't work, and content flashes and shifts.
You're not alone: hydration errors are one of the most common frustrations in Nuxt development, causing layout shifts, broken interactivity, and in severe cases even crashing your entire app in ways that can be remarkably difficult to debug.
Here's what's happening and exactly how to fix it.
To fix these errors, you need to understand what hydration actually is. (The official Nuxt hydration best practices guide covers this in depth if you want to dive deeper.)
Hydration is the process where Vue "takes over" the server-rendered HTML and makes it interactive.
The process happens in three stages:
So what goes wrong?
Hydration expects the client-rendered DOM to match the server HTML exactly. When they don't match, Vue has to throw away the server HTML and rebuild everything from scratch.
The mismatch can be a single character, an attribute, or entire elements.
And the consequences add up fast. Performance hits from forced re-renders, layout shifts that hurt your UX, and content that flashes before correcting itself (which never looks professional).
But accumulating these errors can escalate to gnarly 500 crashes that take down your entire app.
Let's look at the most common causes and their fixes.
Skip to your issue:
This applies if you're using window, document, localStorage, or any other API that only exists in the browser anywhere in your component:
const theme = localStorage.getItem('theme')
const screenWidth = window.innerWidth
These APIs don't exist on the server, so the server renders undefined while the client renders actual values.
You have three ways to fix this:
<ClientOnly> componentonMounted hookFirst, you can wrap your component in <ClientOnly> to skip server rendering entirely:
<ClientOnly fallback="Loading...">
<ThemeToggle />
</ClientOnly>
The second option is to use the onMounted hook to access browser APIs only after hydration:
<script setup>
const screenWidth = ref(0)
onMounted(() => {
screenWidth.value = window.innerWidth
})
</script>
Third, you can use SSR-safe composables that work on both server and client:
// Instead of localStorage
const theme = useCookie('theme')
Default to useCookie instead of localStorage for any persistent data. It works seamlessly on both server and client without any hydration issues.
This applies if you're displaying dates, times, or time-based content like "Good morning":
<template>
<p>Current time: {{ new Date().toLocaleString() }}</p>
</template>
The server renders at one moment, then the client hydrates milliseconds to seconds later (or in a completely different timezone!).
Here's how to fix it:
<NuxtTime> componentThe cleanest fix is <NuxtTime>:
<NuxtTime
:datetime="new Date()"
locale="en-US"
day="numeric"
month="long"
/>
For relative times like "5 minutes ago", use <NuxtTime> with the relative prop. It handles the server/client mismatch automatically.
If you need more control over formatting, render the time only on the client:
<ClientOnly fallback="Loading time...">
{{ formattedTime }}
</ClientOnly>
This applies if you're using Math.random(), crypto.randomUUID(), or generating IDs in templates:
<div :id="`element-${Math.random()}`">
Random ID: {{ Math.random() }}
</div>
The problem here is that the server generates one random value, the client generates a different one, similar to the time-based hydration errors.
Here's how to fix it:
If you need unique IDs for form elements or accessibility attributes, use Vue's useId composable (added in Vue 3.5):
<script setup>
const id = useId()
</script>
<template>
<label :for="id">Name:</label>
<input :id="id" type="text" />
</template>
For other random values, use useState to generate once and reuse:
<script setup>
const randomValue = useState('my-random', () => crypto.randomUUID())
</script>
<template>
<div>{{ randomValue }}</div>
</template>
Both approaches work because they generate the value once on the server and ensure the client hydrates with the exact same value.
This applies if you're using viewport-based conditionals like v-if="screenWidth > 768":
<div v-if="window?.innerWidth > 768">Desktop content</div>
<div v-else>Mobile content</div>
The server has no concept of viewport size, so it renders one version while the client may render the other.
Here's how to fix it:
<ClientOnly>Reach for CSS first:
<div class="hidden md:block">Desktop content</div>
<div class="block md:hidden">Mobile content</div>
CSS media queries don't cause hydration errors because the server sends both versions and the browser hides the wrong one.
When CSS isn't enough (maybe you need to load different data or components), combine onMounted with <ClientOnly>:
<script setup>
const isDesktop = ref(false)
onMounted(() => {
isDesktop.value = window.innerWidth > 768
})
</script>
<ClientOnly>
<div v-if="isDesktop">Desktop only</div>
</ClientOnly>
The key here is that we're only hiding or showing content on the client, never on the server.
This applies if you're using libraries that aren't SSR-aware, like maps, editors, or analytics tools.
Here's how to fix it:
onMountedStart by checking if the library has SSR options built in. Many do, so always check their docs before reaching for workarounds.
If the library isn't SSR-compatible, the Nuxt Scripts module is the preferred solution. It handles SSR compatibility automatically and provides a clean API for loading third-party scripts.
You can also create a client-only plugin by using the *.client.ts suffix in the filename:
export default defineNuxtPlugin(() => {
const library = initializeLibrary()
return { provide: { myLibrary: library } }
})
Or dynamically import the library in onMounted:
<script setup>
onMounted(async () => {
const { MapLibrary } = await import('map-library')
new MapLibrary('#map')
})
</script>
This applies if you're using $fetch directly in your setup function instead of useFetch:
<script setup>
const data = ref(null)
data.value = await $fetch('/api/data')
</script>
This fetches data twice (once on server, once on client) and may return different results.
The fix is simple:
<script setup>
const { data } = await useFetch('/api/data')
</script>
Why does this work?
The useFetch composable fetches once on the server, serializes the result to the payload, and then hydrates the client with the exact same data, while $fetch will run on both the server and the client.
Learn more about when to use $fetch, useFetch, or useAsyncData.
When you encounter a hydration error you can't immediately identify, here's how to track it down.
Error messages tell you which element mismatched. Look for "expected" vs "received" in the message.
The stack trace often points directly to the problematic component, saving you from hunting through your entire codebase (though sometimes the stack trace can be misleading or confusing to read).
pnpm dlx nuxi module add html-validator
Invalid HTML (like nested <p> tags) causes browsers to "fix" the structure automatically, which creates mismatches even when your code is technically identical on both server and client.
The @nuxtjs/html-validator catches these sneaky issues before they become hydration errors.
npx nuxt module add hints
Among other useful debugging tools, it provides a visual diff of server vs client HTML right in the browser. It won't magically solve the problem for you, but it helps you zero in on exactly which part of your code is causing the issue.
export default defineNuxtConfig({
vite: {
define: {
__VUE_PROD_HYDRATION_MISMATCH_DETAILS__: true
}
}
})
Warning: This increases bundle size. Only use it for debugging production issues, then remove it.
When all else fails, use the brute force approach:
It's tedious, but it works when nothing else does.
Now that you understand how to fix hydration errors, you're ready to tackle more advanced Nuxt patterns.
Hydration is just one piece of the SSR puzzle. There's also optimizing data fetching with useAsyncData and useFetch, implementing caching strategies that work across both server and client, and architecting components that are fast by default.
If you want to go deeper, we created Mastering Nuxt to teach you these exact skills.
You'll build a production-ready AI chat application from scratch, learning full-stack patterns including advanced SSR techniques, authentication, database integration, and deployment strategies. It's the kind of real-world project that shows you how all these concepts fit together, not just isolated examples.
Check it out at masteringnuxt.com.
