The library is browser-only. Nuxt's Universal Mode defaults to SSR, so
every table component must be client-only — either via the <ClientOnly>
component, a .client.vue suffix, or a plugin that runs in client mode.
<!-- app.vue -->
<template>
<div>
<h1>Dashboard</h1>
<ClientOnly>
<DataTableView source="/data/trips.csv" />
<template #fallback>
<p>Loading table…</p>
</template>
</ClientOnly>
</div>
</template><!-- components/DataTableView.vue -->
<script setup lang="ts">
import { onMounted, onBeforeUnmount, ref, watch } from 'vue';
import { createDataTable, type DataTable } from '@jeyabbalas/data-table';
import '@jeyabbalas/data-table/styles';
const props = defineProps<{ source: string }>();
const host = ref<HTMLElement | null>(null);
let table: DataTable | undefined;
onMounted(async () => {
if (host.value) {
table = await createDataTable({ container: host.value, source: props.source });
}
});
watch(
() => props.source,
async (next) => {
if (table && !table.isDestroyed()) await table.loadData(next);
},
);
onBeforeUnmount(async () => {
if (table && !table.isDestroyed()) await table.destroy();
});
</script>
<template>
<div ref="host" style="height: 600px" />
</template>The height on the host is a requirement, not styling. The table virtualizes against the container's measured height and renders only the rows that fit; an unbounded container silently defeats virtualization. See Sizing the container.
Nuxt auto-imports components from components/, so <DataTableView> is
available anywhere without an explicit import.
Rename the component to DataTableView.client.vue and Nuxt will only
mount it on the client — no <ClientOnly> wrapper needed:
components/
DataTableView.client.vue
<!-- app.vue -->
<template>
<DataTableView source="/data/trips.csv" />
</template>Compared to <ClientOnly>, the .client.vue approach is less flexible
(the whole file is client-only) but more ergonomic for table-heavy
dashboards.
If multiple pages need a shared manager (e.g., FilterPresetManager),
create it in a client-only plugin:
// plugins/data-table.client.ts
import { FilterPresetManager, SessionStore } from '@jeyabbalas/data-table';
export default defineNuxtPlugin(() => {
const presets = new FilterPresetManager();
const store = new SessionStore();
return {
provide: { dataTable: { presets, store } },
};
});Use inside a component:
<script setup lang="ts">
const { $dataTable } = useNuxtApp();
onMounted(async () => {
await $dataTable.store.open();
table = await createDataTable({
container: host.value!,
source,
presets: { manager: $dataTable.presets },
persistence: { sessionStore: $dataTable.store },
});
});
</script>The .client.ts suffix ensures the plugin only runs on the client,
skipping it entirely during SSR.
Place data under public/:
public/
data/
trips.csv
Reference them with absolute paths — Nuxt serves public/ at the site
root.
Same pattern as Next.js — drop DuckDB WASM bundles into public/duckdb/
and point bridgeOptions.duckdbBundles at them. See
CSP and offline.
Add to nuxt.config.ts:
export default defineNuxtConfig({
nitro: {
routeRules: {
'/**': {
headers: {
'Content-Security-Policy': "script-src 'self'; worker-src 'self';",
},
},
},
},
});Adjust to match your app's other CSP requirements.
- Forgetting
<ClientOnly>or.client.vue. Trying to render the table during SSR causes errors like "document is not defined" — the library usesdocument/window/Workerdirectly. useAsyncDatadoesn't help. The library isn't a data fetcher; it mounts DOM and opens a worker. Don't wrapcreateDataTableinuseAsyncData.- Module auto-import weirdness. Nuxt auto-imports
useNuxtAppbut not the@jeyabbalas/data-tablesymbols. Import those explicitly. - HMR of
.client.vuecomponents. Hot-module reload re-runs the component's setup; make sure youronBeforeUnmountfires cleanly. Check forDestroyedErrorwarnings in the console.
- Vue 3: Vue integration for the underlying mount pattern
- CSP / offline: CSP and offline guide
- Vite: Vite integration (Nuxt 3 uses Vite by default)