Server rendering
Render Shaders in Next.js, Nuxt, SvelteKit, SolidStart and other SSR apps by keeping each shader on the client.
Shaders draws with WebGPU, which only exists in the browser. It can't run during server-side rendering, so every <Shader> has to start on the client. Each meta-framework has its own way to say so; find yours below.
Using Next.js
In the Next.js App Router, put the shader in its own file with 'use client' at the top:
// components/MyShader.jsx
'use client'
import { Shader, LinearGradient, CursorTrail } from 'shaders/react'
export default function MyShader() {
return (
<Shader className="w-full h-64">
<LinearGradient colorA="#0f172a" colorB="#7c3aed" />
<CursorTrail />
</Shader>
)
}
Then render it from a Server Component:
// app/page.jsx (Server Component)
import MyShader from '@/components/MyShader'
export default function Page() {
return (
<main>
<MyShader />
<h1>Your content</h1>
</main>
)
}
'use client' makes it a Client Component: it hydrates and runs in the browser. Next.js still prerenders Client Components on the server, which <Shader> handles: it renders an empty canvas there and starts WebGPU only after it mounts. If your own component touches browser APIs while it renders, use the client-only import below or a mounted guard.
Turning off SSR with next/dynamic
To skip the server render entirely, load the component with next/dynamic and ssr: false. In the App Router that call has to live in a Client Component, so give it a small wrapper:
// components/MyShaderClient.jsx
'use client'
import dynamic from 'next/dynamic'
const MyShader = dynamic(
() => import('@/components/MyShader'),
{ ssr: false }
)
export default function MyShaderClient() {
return <MyShader />
}
Then render the wrapper from your page:
// app/page.jsx (Server Component)
import MyShaderClient from '@/components/MyShaderClient'
export default function Page() {
return (
<main>
<MyShaderClient />
</main>
)
}
The shader renders only in the browser, and its module never loads on the server. In the Pages Router, call dynamic with ssr: false directly in the page.
Adding a shader to the root layout
For a shader on every page, such as a full-page background, render a client component from the root layout:
// app/layout.jsx
import BackgroundShader from '@/components/BackgroundShader'
export default function RootLayout({ children }) {
return (
<html>
<body>
<BackgroundShader />
{children}
</body>
</html>
)
}
The background component carries 'use client' and sits behind the page:
// components/BackgroundShader.jsx
'use client'
import { Shader, Aurora } from 'shaders/react'
export default function BackgroundShader() {
return (
<Shader className="fixed inset-0 -z-10 pointer-events-none">
<Aurora />
</Shader>
)
}
Using React SSR without Next.js
Without Next.js, render the shader only after the component has mounted:
import { useState, useEffect } from 'react'
import { Shader, LinearGradient } from 'shaders/react'
export default function MyShader() {
const [mounted, setMounted] = useState(false)
useEffect(() => {
setMounted(true)
}, [])
// effects don't run on the server, so SSR renders nothing here
if (!mounted) return null
return (
<Shader className="w-full h-64">
<LinearGradient />
</Shader>
)
}
Using Nuxt
In Nuxt, wrap the <Shader> in the built-in <ClientOnly> component:
<ClientOnly>
<Shader class="w-full h-64">
<LinearGradient />
</Shader>
</ClientOnly>
The server renders nothing in its place, and the shader mounts on the client after hydration. A canvas has no content for Vue to compare, so it never causes a hydration mismatch warning.
Showing a placeholder while it loads
Add a #fallback slot to show something until the shader mounts:
<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>
Give the fallback the same size as the shader to avoid layout shift.
Registering components in a client-only plugin
You can also register the components globally from a plugin that only runs in the browser:
// 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 makes Nuxt run this plugin only in the browser. The server doesn't know these components, so keep them inside <ClientOnly>.
Using Vue SSR without Nuxt
Without Nuxt, render the shader with v-if on a mounted flag. To keep the Shaders bundle out of SSR too, load the components with defineAsyncComponent:
<script setup>
import { defineAsyncComponent, ref, onMounted } from 'vue'
// onMounted never runs on the server, so the flag stays false during SSR
const isMounted = ref(false)
onMounted(() => { isMounted.value = true })
// fetched in the browser the first time they render
const Shader = defineAsyncComponent(() => import('shaders/vue').then(m => m.Shader))
const LinearGradient = defineAsyncComponent(() => import('shaders/vue').then(m => m.LinearGradient))
</script>
<template>
<div class="w-full h-64">
<Shader v-if="isMounted" class="w-full h-64">
<LinearGradient />
</Shader>
</div>
</template>
Using SvelteKit
There's nothing to set up. Use <Shader> as you would on any other page:
<script>
import { Shader, LinearGradient } from 'shaders/svelte'
</script>
<Shader class="w-full h-64">
<LinearGradient />
</Shader>
On the server, <Shader> renders an empty wrapper with no canvas and no error. The canvas and the GPU setup are added on the client when the component mounts. Your own component script still runs on the server, so put code that touches the DOM in onMount.
Rendering a placeholder with browser
To choose what the server renders, check browser from SvelteKit's $app/environment:
<script>
import { browser } from '$app/environment'
import { Shader, LinearGradient } from 'shaders/svelte'
</script>
{#if browser}
<Shader class="w-full h-64">
<LinearGradient />
</Shader>
{:else}
<!-- Optional server-rendered placeholder -->
<div class="w-full h-64 bg-gray-900 rounded"></div>
{/if}
Give the placeholder the same size as the shader to avoid layout shift.
Lazy-loading the components
To keep the Shaders bundle off the server entirely, import it in onMount:
<script>
import { onMount } from 'svelte'
let ShaderComponent
let LinearGradientComponent
onMount(async () => {
const { Shader, LinearGradient } = await import('shaders/svelte')
ShaderComponent = Shader
LinearGradientComponent = LinearGradient
})
</script>
{#if ShaderComponent}
<svelte:component this={ShaderComponent} class="w-full h-64">
<svelte:component this={LinearGradientComponent} />
</svelte:component>
{/if}
Using SolidStart
In SolidStart, put the shader in its own component:
// components/MyShader.jsx
import { Shader, LinearGradient } from 'shaders/solid'
export default function MyShader() {
return (
<Shader class="w-full h-64">
<LinearGradient colorA="#0f172a" colorB="#7c3aed" />
</Shader>
)
}
Then load it with SolidStart's clientOnly:
// routes/index.jsx
import { clientOnly } from '@solidjs/start'
const MyShader = clientOnly(() => import('../components/MyShader'))
export default function Page() {
return (
<main>
<MyShader />
<h1>Your content</h1>
</main>
)
}
clientOnly wraps the component in a Suspense boundary and renders it only in the browser. During SSR it returns null.
Using Solid SSR without SolidStart
Without SolidStart, render the shader only after the component mounts, by setting a signal in onMount:
import { createSignal, onMount } from 'solid-js'
import { Shader, LinearGradient } from 'shaders/solid'
export default function MyShader() {
const [mounted, setMounted] = createSignal(false)
// onMount never runs on the server, so the signal stays false during SSR
onMount(() => setMounted(true))
return (
<div class="w-full h-64">
{mounted() && (
<Shader class="w-full h-64">
<LinearGradient />
</Shader>
)}
</div>
)
}
Using plain JavaScript
createShader only runs where you call it, so there's nothing to turn off. Call it from code that runs in the browser, such as a script on the page or your framework's client-side hook, and the server never touches the GPU.
Next steps
- Quickstart: install the package and render your first effect.
- Layout and positioning: place the canvas behind or around your UI.
- Performance: what each layer costs and how to keep effects fast.