Use Handsontable in Nuxt
Handsontable relies on browser APIs that are not available during server-side rendering. This page explains how to integrate Handsontable into a Nuxt 3 application without SSR errors.
Why Handsontable needs client-side rendering
Nuxt 3 renders Vue components on the server before sending HTML to the browser. That server environment has no DOM — no window, no document, no HTMLElement. Handsontable’s initialization code and the underlying Walkontable rendering engine access these browser globals.
When Nuxt runs a component that imports or initializes Handsontable on the server, you get errors like:
ReferenceError: window is not definedThe fix is to keep all Handsontable code out of the server rendering path.
Prerequisites
-
Nuxt 3 project set up and running.
-
Handsontable and the Vue 3 wrapper installed:
Terminal window npm install handsontable @handsontable/vue3
Using <ClientOnly>
Nuxt’s built-in <ClientOnly> component prevents its children from rendering on the server. Wrap your <HotTable> in it:
<template> <ClientOnly> <HotTable :settings="gridSettings" /> <template #fallback> <p>Loading the data grid...</p> </template> </ClientOnly></template>
<script setup lang="ts">import { ref } from 'vue';import { HotTable } from '@handsontable/vue3';import { registerAllModules } from 'handsontable/registry';import type { GridSettings } from 'handsontable/settings';
registerAllModules();
const gridSettings = ref<GridSettings>({ data: [ ['Mercedes', 'Germany', 2000, 2023], ['BMW', 'Germany', 1998, 2024], ['Toyota', 'Japan', 2001, 2023], ['Honda', 'Japan', 1999, 2022], ['Ford', 'USA', 1997, 2024], ], colHeaders: ['Brand', 'Country', 'Since', 'Until'], rowHeaders: true, licenseKey: 'non-commercial-and-evaluation',});</script>The #fallback slot renders on the server in place of the grid. Use it to show a placeholder or skeleton so the page has visible content before the browser takes over.
Using the .client.vue suffix
Nuxt treats any component file ending in .client.vue as a client-only component — it is excluded from the server bundle entirely, including its imports. This is the most robust option when a package accesses browser APIs at module load time.
Create a wrapper component named with the .client.vue suffix:
components/└── HandsonGrid.client.vue<template> <HotTable :settings="props.settings" /></template>
<script setup lang="ts">import { HotTable } from '@handsontable/vue3';import { registerAllModules } from 'handsontable/registry';import type { GridSettings } from 'handsontable/settings';
registerAllModules();
const props = defineProps<{ settings: GridSettings;}>();</script>Use it in your page without any additional wrapper:
<template> <HandsonGrid :settings="gridSettings" /></template>
<script setup lang="ts">import { ref } from 'vue';import type { GridSettings } from 'handsontable/settings';
const gridSettings = ref<GridSettings>({ data: [ ['Mercedes', 'Germany', 2000, 2023], ['BMW', 'Germany', 1998, 2024], ['Toyota', 'Japan', 2001, 2023], ['Honda', 'Japan', 1999, 2022], ['Ford', 'USA', 1997, 2024], ], colHeaders: ['Brand', 'Country', 'Since', 'Until'], rowHeaders: true, licenseKey: 'non-commercial-and-evaluation',});</script>nuxt.config.ts notes
For most Nuxt 3 projects (which use Vite by default), no extra configuration is needed. <ClientOnly> or .client.vue is sufficient.
In rare cases — for example, when using a Nuxt plugin that imports Handsontable in a server-side context — you can add the packages to build.transpile as a fallback:
export default defineNuxtConfig({ build: { transpile: ['handsontable', '@handsontable/vue3'], },});Start with <ClientOnly> or .client.vue before reaching for this option.
Result
Handsontable renders in the browser. The SSR pass shows the #fallback content (with <ClientOnly>) or nothing (with .client.vue), and the grid initializes once the Vue component mounts on the client.