SSR with Nuxt / Vue
Shaders uses WebGPU, which requires a browser environment. It cannot run during server-side rendering. This page covers how to safely use Shaders in SSR-enabled Vue applications.
Nuxt: <ClientOnly>
Nuxt provides a built-in <ClientOnly> component that prevents its children from rendering on the server. This is the simplest approach:
<ClientOnly>
<Shader class="w-full h-64">
<LinearGradient />
</Shader>
</ClientOnly>
The shader renders nothing on the server and mounts on the client after hydration. Your page structure and layout are not affected.
You can add a placeholder slot to show something while the client-side shader loads:
<ClientOnly>
<Shader class="w-full h-64">
<LinearGradient />
</Shader>
<template #fallback>
<div class="w-full h-64 bg-gray-900 animate-pulse rounded" />
</template>
</ClientOnly>
Vue SSR (without Nuxt)
If you're using Vue SSR without Nuxt, use v-if with a mounted flag:
<script setup>
import { ref, onMounted } from 'vue'
const isMounted = ref(false)
onMounted(() => { isMounted.value = true })
</script>
<template>
<div class="w-full h-64">
<Shader v-if="isMounted" class="w-full h-64">
<LinearGradient />
</Shader>
</div>
</template>
Dynamic import for code-splitting
To avoid loading the Shaders bundle during SSR and defer it to the client, use a lazy dynamic import:
<script setup>
import { defineAsyncComponent, ref, onMounted } from 'vue'
const isMounted = ref(false)
onMounted(() => { isMounted.value = true })
// Lazy-load shader components — only fetched client-side when rendered
const Shader = defineAsyncComponent(() => import('shaders/vue').then(m => m.Shader))
const LinearGradient = defineAsyncComponent(() => import('shaders/vue').then(m => m.LinearGradient))
</script>
<template>
<Shader v-if="isMounted">
<LinearGradient />
</Shader>
</template>
Or in Nuxt, use defineNuxtPlugin or a client-only plugin to lazy-load:
// plugins/shaders.client.ts
import { defineNuxtPlugin } from '#app'
import { Shader, LinearGradient } from 'shaders/vue'
export default defineNuxtPlugin((nuxtApp) => {
nuxtApp.vueApp.component('Shader', Shader)
nuxtApp.vueApp.component('LinearGradient', LinearGradient)
})
The .client.ts suffix ensures this plugin only runs in the browser.
Why no hydration mismatch?
The <Shader> component renders a <canvas> element. Canvas elements have no server-rendered HTML output — there's nothing for Vue to hydrate or compare. This means there's no hydration mismatch warning even if the canvas appears in the initial HTML. That said, the GPU initialization still requires a browser, so the <ClientOnly> wrapper (or mounted guard) is still needed to prevent SSR errors.