Skip to main content

Server-Side Rendering

Vuetify 4 provides comprehensive SSR support with built-in configurations for Nuxt and custom SSR frameworks.

SSR Options

The ssr option in Vuetify’s framework configuration controls server-side rendering behavior:
export type SSROptions = boolean | {
  clientWidth: number
  clientHeight?: number
}

Basic Configuration

import { createVuetify } from 'vuetify'

export default createVuetify({
  ssr: true,
})

Advanced Configuration

Provide initial viewport dimensions for more accurate SSR rendering:
import { createVuetify } from 'vuetify'

export default createVuetify({
  ssr: {
    clientWidth: 1920,
    clientHeight: 1080,
  },
})
Providing initial dimensions helps prevent layout shifts during hydration by rendering components with realistic viewport sizes on the server.

Display Composable with SSR

The createDisplay function in Vuetify handles SSR-aware viewport detection:
// From packages/vuetify/src/composables/display.ts
export function createDisplay(
  options?: DisplayOptions,
  ssr?: SSROptions
): DisplayInstance {
  const { thresholds, mobileBreakpoint } = parseDisplayOptions(options)
  
  const height = shallowRef(getClientHeight(ssr))
  const platform = shallowRef(getPlatform(ssr))
  const width = shallowRef(getClientWidth(ssr))
  
  return { ...toRefs(state), update, ssr: !!ssr }
}

Client Width Detection

function getClientWidth(ssr?: SSROptions) {
  return IN_BROWSER && !ssr
    ? window.innerWidth
    : (typeof ssr === 'object' && ssr.clientWidth) || 0
}

function getClientHeight(ssr?: SSROptions) {
  return IN_BROWSER && !ssr
    ? window.innerHeight
    : (typeof ssr === 'object' && ssr.clientHeight) || 0
}
During SSR:
  • If ssr: true, dimensions default to 0
  • If ssr: { clientWidth: 1920 }, uses provided dimensions
  • On client, uses actual window dimensions

Platform Detection

function getPlatform(ssr?: SSROptions): DisplayPlatform {
  const userAgent = IN_BROWSER && !ssr
    ? window.navigator.userAgent
    : 'ssr'
  
  return {
    android: match(/android/i),
    ios: match(/iphone|ipad|ipod/i),
    chrome: match(/chrome/i),
    // ... other platform checks
    ssr: userAgent === 'ssr',
  }
}

Nuxt Integration

Vuetify automatically detects Nuxt and integrates with its lifecycle:
// From packages/vuetify/src/framework.ts
if (IN_BROWSER && options.ssr) {
  if (app.$nuxt) {
    app.$nuxt.hook('app:suspense:resolve', () => {
      display.update()
    })
  } else {
    const { mount } = app
    app.mount = (...args) => {
      const vm = mount(...args)
      nextTick(() => display.update())
      app.mount = mount
      return vm
    }
  }
}

Nuxt 3 Setup

Create a Vuetify plugin:
// plugins/vuetify.ts
import { createVuetify } from 'vuetify'
import * as components from 'vuetify/components'
import * as directives from 'vuetify/directives'

export default defineNuxtPlugin((nuxtApp) => {
  const vuetify = createVuetify({
    ssr: true,
    components,
    directives,
  })
  
  nuxtApp.vueApp.use(vuetify)
})
Configure Nuxt:
// nuxt.config.ts
export default defineNuxtConfig({
  build: {
    transpile: ['vuetify'],
  },
  css: ['vuetify/styles'],
})
Make sure to add vuetify to the transpile array in Nuxt config. This ensures Vuetify’s ESM modules are properly processed during SSR.

Vite SSR

Vuetify works seamlessly with Vite’s SSR mode:
// server.ts
import { createSSRApp } from 'vue'
import { createVuetify } from 'vuetify'

export function createApp() {
  const app = createSSRApp(App)
  
  const vuetify = createVuetify({
    ssr: true,
  })
  
  app.use(vuetify)
  
  return { app, vuetify }
}

Entry Client

// entry-client.ts
import { createApp } from './main'

const { app } = createApp()

app.mount('#app')

Entry Server

// entry-server.ts
import { renderToString } from 'vue/server-renderer'
import { createApp } from './main'

export async function render() {
  const { app } = createApp()
  const html = await renderToString(app)
  return html
}

Hydration Composable

Vuetify provides a hydration-aware composable:
// From packages/vuetify/src/composables/hydration.ts
export function useHydration() {
  const { ssr } = useDisplay()
  
  if (ssr) {
    const isMounted = shallowRef(false)
    onMounted(() => {
      isMounted.value = true
    })
    return { isMounted }
  }
  
  return { isMounted: ref(true) }
}
Use in components to avoid hydration mismatches:
<script setup>
import { useHydration } from 'vuetify'

const { isMounted } = useHydration()
</script>

<template>
  <div>
    <div v-if="!isMounted">Loading...</div>
    <div v-else>{{ new Date().toLocaleString() }}</div>
  </div>
</template>

SSR Boot Styles

Vuetify includes a composable for handling styles before hydration:
// From packages/vuetify/src/composables/ssrBoot.ts
export function useSSRBoot() {
  const isBooted = shallowRef(false)
  
  onMounted(() => {
    window.requestAnimationFrame(() => {
      isBooted.value = true
    })
  })
  
  const ssrBootStyles = computed(() => !isBooted.value ? ({
    transition: 'none !important',
  }) : undefined)
  
  return { ssrBootStyles, isBooted: readonly(isBooted) }
}
This prevents flash of unstyled content (FOUC) by disabling transitions until hydration completes.

Responsive Design with SSR

Handle responsive layouts correctly with SSR:
<script setup>
import { useDisplay } from 'vuetify'

const { mobile, mdAndUp } = useDisplay()
</script>

<template>
  <v-container>
    <v-row v-if="mdAndUp">
      <!-- Desktop layout -->
    </v-row>
    <v-row v-else>
      <!-- Mobile layout -->
    </v-row>
  </v-container>
</template>
When using display breakpoints with SSR, consider providing realistic clientWidth values to minimize layout shifts during hydration.

Common SSR Issues

Window is not defined

Ensure code accessing browser APIs is wrapped:
import { IN_BROWSER } from 'vuetify'

if (IN_BROWSER) {
  // Browser-only code
  window.addEventListener('resize', handleResize)
}

Hydration Mismatches

Avoid rendering different content on server vs client:
<!-- Bad: Will cause hydration mismatch -->
<div>{{ new Date().toISOString() }}</div>

<!-- Good: Only render after mount -->
<div v-if="isMounted">{{ new Date().toISOString() }}</div>

Missing Styles

Ensure styles are included in SSR build:
// vite.config.ts
export default {
  ssr: {
    noExternal: ['vuetify'],
  },
}

Performance Optimization

Provide accurate viewport dimensions for better SSR performance:
// Server-side detection from request headers
import { createVuetify } from 'vuetify'

export function createVuetifyInstance(req) {
  const clientWidth = parseInt(req.headers['viewport-width']) || 1920
  
  return createVuetify({
    ssr: { clientWidth },
  })
}

Build docs developers (and LLMs) love