SSR / Nuxt
<VSelect> is server-rendered without special configuration. The IDs come from Vue 3.5's useId(), so there are no hydration mismatches; Floating UI only positions client-side; the listbox only renders when opened.
Nuxt 3
Install
npm install @vue-select-plus/vue @vue-select-plus/stylesRegister globally
// plugins/vue-select-plus.ts
import { VSelect } from '@vue-select-plus/vue';
export default defineNuxtPlugin((nuxt) => {
nuxt.vueApp.component('VSelect', VSelect);
});Import the stylesheet
// nuxt.config.ts
export default defineNuxtConfig({
css: ['@vue-select-plus/vue/styles.css']
});Use anywhere
<script setup lang="ts">
import type { SelectOption } from '@vue-select-plus/vue';
const value = ref<string | null>(null);
const options: SelectOption[] = [
{ value: 'a', label: 'A' },
{ value: 'b', label: 'B' }
];
</script>
<template>
<VSelect v-model="value" :options label="Pick one" clearable />
</template>No auto-import config needed — VSelect is registered as a global component above. If you'd rather auto-import, use components.dirs and re-export from your own components folder.
Teleport and SSR
By default the listbox teleports to <body> when opened. On the server, <body> doesn't exist yet — Vue handles this gracefully and renders nothing for the menu (the menu only opens client-side anyway). No special handling required.
If you render the component server-side and the user has JavaScript disabled, the trigger + label + selected-value summary are still rendered as HTML and the form value is serialized via the hidden inputs. The dropdown won't be openable, but the data is on the page.
Astro
---
import { VSelect } from '@vue-select-plus/vue';
import '@vue-select-plus/vue/styles.css';
---
<VSelect client:load options={fruits} label="Fruit" />Use client:load (or client:visible for below-the-fold instances) — the component needs JS for the interactive parts. Server-rendered shell is the same as Nuxt.
Hydration tips
- IDs are deterministic per app. Different
<VSelect>instances on the same page get different ids derived from Vue'suseId(). Across page reloads the ids may differ, but they always match between server and client renders of the same instance — that's what matters for hydration. - Don't pass a random
idprop like:id="Math.random()"— that defeats the purpose. Either omitidentirely (the component generates a stable one) or use a meaningful slug. teleport: 'body'(the default) requires no setup. If you teleport into a custom container (e.g. a modal), make sure that container exists before the menu opens — pass a ref via:teleport="modalRef".
Testing under SSR
We ship SSR smoke tests that call renderToString() on every important configuration (single, multi, searchable, required+error, hidden form inputs). If you're building a wrapper component, mirror that pattern:
import { renderToString } from '@vue/server-renderer';
import { createSSRApp } from 'vue';
import MyWrapper from './MyWrapper.vue';
const html = await renderToString(createSSRApp(MyWrapper));
expect(html).toContain('role="combobox"');Known SSR caveats
prefers-color-schemeis a client-side media query. During SSR we don't know which theme the user prefers; the rendered HTML uses the default (light) theme. The dark theme kicks in after hydration once the browser evaluates the media query. To avoid a brief light-mode flash, set thedarkorvsp-darkclass on<html>server-side based on a cookie orprefers-color-schemeheaders.forced-colorsis also client-side. Same advice.@floating-ui/vue'sautoUpdateattaches listeners on mount. If you SSR-render with:loading="true"andisOpenwere ever true server-side (it isn't by default), Floating UI would error. Don't forceisOpen: truevia adefineModelworkaround during SSR.