Skip to content

Vue Integration

The @toolbox-web/grid-vue package provides Vue 3 integration for the <tbw-grid> data grid component.

Vue versionSupport level
3.5+Tested — used in demos and CI
3.3 – 3.4Supported (minimum peer dependency)
< 3.3Not supported
Vue 2Not supported — adapter requires Vue 3 Composition API
Terminal window
npm install @toolbox-web/grid @toolbox-web/grid-vue
yarn add @toolbox-web/grid @toolbox-web/grid-vue
pnpm add @toolbox-web/grid @toolbox-web/grid-vue
bun add @toolbox-web/grid @toolbox-web/grid-vue

Register the grid web component and configure Vue to recognize custom elements:

main.ts
import '@toolbox-web/grid';
// Enable features via side-effect imports
// Only import the features you use - this keeps your bundle small!
import '@toolbox-web/grid-vue/features/selection';
import '@toolbox-web/grid-vue/features/editing';
import '@toolbox-web/grid-vue/features/multi-sort';
import '@toolbox-web/grid-vue/features/filtering';

Important: Configure Vue to treat tbw-* tags as custom elements in your vite.config.ts:

vite.config.ts
import vue from '@vitejs/plugin-vue';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [
vue({
template: {
compilerOptions: {
// Tell Vue that tbw-* tags are web components
isCustomElement: (tag) => tag.startsWith('tbw-'),
},
},
}),
],
});

The simplest way to use the grid is with the TbwGrid component and feature props:

<script setup lang="ts">
// Enable features you use
import '@toolbox-web/grid-vue/features/selection';
import '@toolbox-web/grid-vue/features/editing';
import '@toolbox-web/grid-vue/features/multi-sort';
import '@toolbox-web/grid-vue/features/filtering';
import { TbwGrid } from '@toolbox-web/grid-vue';
import type { ColumnConfig } from '@toolbox-web/grid';
import { ref } from 'vue';
interface Employee {
id: number;
name: string;
department: string;
salary: number;
}
const employees = ref<Employee[]>([
{ id: 1, name: 'Alice', department: 'Engineering', salary: 95000 },
{ id: 2, name: 'Bob', department: 'Marketing', salary: 75000 },
{ id: 3, name: 'Charlie', department: 'Sales', salary: 85000 },
]);
const columns: ColumnConfig<Employee>[] = [
{ field: 'id', header: 'ID', type: 'number', width: 70 },
{ field: 'name', header: 'Name', editable: true, sortable: true },
{ field: 'department', header: 'Department', editable: true, sortable: true },
{ field: 'salary', header: 'Salary', type: 'number', format: (v: number) => '$' + v.toLocaleString() },
];
</script>
<template>
<TbwGrid
:rows="employees"
:columns="columns"
selection="range"
editing="dblclick"
:multi-sort="true"
:filtering="{ debounceMs: 200 }"
style="height: 400px; display: block"
/>
</template>

Each feature prop enables a specific plugin with simplified configuration:

Feature PropFeature ImportDescription
selectionfeatures/selectionCell, row, or range selection
editingfeatures/editingInline cell editing
multi-sortfeatures/multi-sortMulti-column sorting
filteringfeatures/filteringColumn filtering
clipboardfeatures/clipboardCopy/paste support
context-menufeatures/context-menuRight-click context menu
reorder-columnsfeatures/reorder-columnsColumn drag-to-reorder
visibilityfeatures/visibilityColumn visibility panel
pinned-columnsfeatures/pinned-columnsSticky left/right columns
pinned-rowsfeatures/pinned-rowsSticky top/bottom rows
grouping-columnsfeatures/grouping-columnsMulti-level column headers
grouping-rowsfeatures/grouping-rowsRow grouping
column-virtualizationfeatures/column-virtualizationVirtualize columns for wide grids
reorder-rowsfeatures/reorder-rowsRow drag-to-reorder
treefeatures/treeHierarchical tree view
master-detailfeatures/master-detailExpandable detail rows
responsivefeatures/responsiveCard layout for narrow viewports
undo-redofeatures/undo-redoEdit undo/redo
exportfeatures/exportCSV/Excel export
printfeatures/printPrint support
pivotfeatures/pivotPivot table functionality
server-sidefeatures/server-sideServer-side data loading

Core Config Props (no feature import needed):

PropTypeDescription
sortablebooleanGrid-wide sorting toggle (default: true)
filterablebooleanGrid-wide filtering toggle (default: true). Requires FilteringPlugin.
selectablebooleanGrid-wide selection toggle (default: true). Requires SelectionPlugin.

Use the useGrid composable for programmatic control:

<script setup lang="ts">
import { TbwGrid, useGrid } from '@toolbox-web/grid-vue';
import { ref } from 'vue';
interface Employee {
id: number;
name: string;
}
const employees = ref<Employee[]>([/* ... */]);
const { gridElement, forceLayout } = useGrid();
function refreshGrid() {
forceLayout();
}
async function exportCsv() {
const grid = gridElement.value;
if (!grid) return;
const exportPlugin = grid.getPluginByName('export');
await exportPlugin?.exportToCsv({ filename: 'employees.csv' });
}
</script>
<template>
<div>
<button @click="refreshGrid">Refresh</button>
<button @click="exportCsv">Export CSV</button>
<TbwGrid :rows="employees" :columns="columns" />
</div>
</template>

Feature imports export scoped composables for type-safe programmatic access to plugin functionality:

<script setup lang="ts">
import '@toolbox-web/grid-vue/features/export';
import { useGridExport } from '@toolbox-web/grid-vue/features/export';
import { TbwGrid } from '@toolbox-web/grid-vue';
import { ref } from 'vue';
const employees = ref<Employee[]>([/* ... */]);
const { exportToCsv, exportToExcel, isExporting } = useGridExport();
</script>
<template>
<div>
<button @click="exportToCsv('employees.csv')" :disabled="isExporting()">Export CSV</button>
<TbwGrid :rows="employees" :columns="columns" export />
</div>
</template>

See the API Reference > Composables section for detailed method signatures, return types, and examples.

All composables accept an optional selector parameter to target a specific grid via DOM query instead of Vue’s provide/inject. Use when a component contains multiple grids:

<script setup>
import { useGridSelection } from '@toolbox-web/grid-vue/features/selection';
// Target a specific grid by CSS selector
const selection = useGridSelection('tbw-grid.primary');
</script>
ComposableImportKey Methods
useGridSelection()features/selectionselectAll, clearSelection, getSelection, getSelectedRows
useGridFiltering()features/filteringsetFilter, clearAllFilters, getFilters, getFilteredRowCount
useGridExport()features/exportexportToCsv, exportToExcel, exportToJson, isExporting
useGridPrint()features/printprint, isPrinting
useGridUndoRedo()features/undo-redoundo, redo, canUndo, canRedo

Custom Vue editors that open a popover, listbox, calendar, or other floating panel typically render that panel through <Teleport> so it can escape the cell’s overflow clipping. Without help, the grid sees a click into the teleported panel as a click outside the editor and commits-and-exits the row before the user can pick an option.

The useGridOverlay composable bridges that gap by registering the panel with the grid as an external focus container — the Vue equivalent of Angular’s BaseOverlayEditor.initOverlay() and React’s useGridOverlay. Pair it with aria-expanded / aria-controls on the trigger so even editors that forget to call the composable get correct keyboard / pointer behaviour out of the box (the editing plugin honours those attributes as a generic fallback — see issue #251).

<script setup lang="ts">
import { ref, useId } from 'vue';
import { useGridOverlay, type GridEditorContext } from '@toolbox-web/grid-vue';
const props = defineProps<{ ctx: GridEditorContext<MyRow> }>();
const open = ref(false);
const panel = ref<HTMLElement | null>(null);
const listboxId = useId();
// Registers panel.value with the grid while open.value === true; unregisters
// automatically on close or unmount.
useGridOverlay(panel, { open });
</script>
<template>
<input
role="combobox"
:aria-expanded="open"
:aria-controls="listboxId"
:value="ctx.value ?? ''"
@click="open = true"
@keydown.escape="ctx.cancel()"
/>
<Teleport to="body" v-if="open">
<div ref="panel" :id="listboxId" role="listbox" class="my-listbox">
<!-- options that call ctx.commit(option) on click -->
</div>
</Teleport>
</template>

useGridOverlay(panelRef, options?) resolves the owning <tbw-grid> in this order:

  1. options.gridElement if explicitly passed (accepts a MaybeRef<DataGridElement>),
  2. panelRef.value.closest('tbw-grid') for inline (non-teleported) panels,
  3. The GRID_ELEMENT_KEY injection populated by <TbwGrid> / <GridProvider>.

options.open accepts a MaybeRef<boolean> so reactive refs and plain booleans both work. The grid host is also exposed on ColumnEditorContext.grid for editors that need to call grid APIs directly without using the composable.

For custom configurations or third-party plugins, instantiate manually using markRaw():

<script setup lang="ts">
import { TbwGrid, type GridConfig } from '@toolbox-web/grid-vue';
import { SelectionPlugin, EditingPlugin, ClipboardPlugin } from '@toolbox-web/grid/all';
import { markRaw, ref } from 'vue';
const employees = ref<Employee[]>([/* ... */]);
const config = markRaw<GridConfig<Employee>>({
columns: [
{ field: 'id', header: 'ID' },
{ field: 'name', header: 'Name', editable: true },
],
plugins: [
new SelectionPlugin({ mode: 'range', checkbox: true }),
new EditingPlugin({ editOn: 'dblclick' }),
new ClipboardPlugin({ includeHeaders: true }),
],
});
</script>
<template>
<TbwGrid :rows="employees" :grid-config="config" />
</template>

Important: Always use markRaw() for grid configs containing plugins. Vue’s reactivity system can interfere with plugin class instances.

Feature props and manual plugins can be mixed — the grid merges them.

The grid supports two complementary ways to customize icons — see the core Icon Customization reference and the Theming Guide for the full picture:

  • CSS variables (--tbw-icon-*) — preferred for themes and static customization; no JavaScript needed.
  • gridConfig.icons — for dynamic icons, icon libraries, or HTMLElement instances; takes precedence over CSS.

GridIconProvider is the Vue wrapper for the JS path — it injects icons into gridConfig.icons for every descendant <TbwGrid>:

<script setup lang="ts">
import { GridIconProvider, TbwGrid } from '@toolbox-web/grid-vue';
</script>
<template>
<GridIconProvider :icons="{ sortAsc: '↑', sortDesc: '↓' }">
<TbwGrid :rows="employees" :columns="columns" />
</GridIconProvider>
</template>

For backends that own paging / sorting / filtering, use the serverSide feature prop. Your getRows handler receives the current sortModel, filterModel, and block range — return rows and the total count. See ServerSideDataSource, GetRowsParams, and GetRowsResult for the full contract — including the params.signal AbortSignal you can forward to fetch() for explicit cancellation.

<script setup lang="ts">
import '@toolbox-web/grid-vue/features/server-side';
import '@toolbox-web/grid-vue/features/multi-sort';
import { TbwGrid } from '@toolbox-web/grid-vue';
import type { ServerSideDataSource } from '@toolbox-web/grid/plugins/server-side';
const dataSource: ServerSideDataSource<Employee> = {
getRows: async ({ startNode, endNode, sortModel, filterModel, signal }) => {
const params = new URLSearchParams({
from: String(startNode),
to: String(endNode),
});
if (sortModel?.length) {
params.set(
'sort',
sortModel.map((s) => `${s.field}:${s.direction}`).join(','),
);
}
if (filterModel) params.set('filter', JSON.stringify(filterModel));
const response = await fetch(`/api/employees?${params}`, { signal });
const { rows, totalNodeCount } = await response.json();
return { rows, totalNodeCount };
},
};
</script>
<template>
<TbwGrid
:columns="columns"
multi-sort
:server-side="{ dataSource, cacheBlockSize: 100 }"
/>
</template>

If your app already uses TanStack Vue Query, one useful pairing is to back ServerSidePlugin with Vue Query’s request cache. Each block the grid asks for becomes a query keyed by the request shape (startNode, endNode, sortModel, filterModel) — so scrolling back to a previously fetched range is served from cache instead of re-hitting the API, and you get retries, in-flight deduplication, and devtools for free.

The pattern: keep getRows thin and delegate fetching to queryClient.fetchQuery with a stable key per block.

<script setup lang="ts">
import '@toolbox-web/grid-vue/features/server-side';
import '@toolbox-web/grid-vue/features/multi-sort';
import { TbwGrid } from '@toolbox-web/grid-vue';
import type {
GetRowsParams,
GetRowsResult,
ServerSideDataSource,
} from '@toolbox-web/grid/plugins/server-side';
import { useQueryClient } from '@tanstack/vue-query';
async function fetchEmployeeBlock(
params: GetRowsParams,
): Promise<GetRowsResult<Employee>> {
const search = new URLSearchParams({
from: String(params.startNode),
to: String(params.endNode),
});
if (params.sortModel?.length) {
search.set('sort', params.sortModel.map((s) => `${s.field}:${s.direction}`).join(','));
}
if (params.filterModel) search.set('filter', JSON.stringify(params.filterModel));
const response = await fetch(`/api/employees?${search}`, { signal: params.signal });
return response.json();
}
const queryClient = useQueryClient();
const dataSource: ServerSideDataSource<Employee> = {
// Each block becomes a cached query — Vue Query dedupes in-flight
// requests, retries failed ones, and serves repeated scrolls from cache.
getRows: (params) =>
queryClient.fetchQuery({
queryKey: [
'employees',
params.startNode,
params.endNode,
params.sortModel,
params.filterModel,
],
queryFn: ({ signal }) => fetchEmployeeBlock({ ...params, signal }),
staleTime: 60_000,
}),
};
</script>
<template>
<TbwGrid
:columns="columns"
multi-sort
:server-side="{ dataSource, cacheBlockSize: 100 }"
/>
</template>
<script setup lang="ts">
import { TbwGrid } from '@toolbox-web/grid-vue';
import { ref } from 'vue';
const employees = ref<Employee[]>([
{ id: 1, name: 'Alice', department: 'Engineering' },
{ id: 2, name: 'Bob', department: 'Design' },
]);
let nextId = 3;
function handleAdd() {
employees.value = [
...employees.value,
{ id: nextId++, name: 'New Employee', department: 'TBD' },
];
}
function removeFirst() {
employees.value = employees.value.slice(1);
}
</script>
<template>
<div>
<button @click="handleAdd">Add</button>
<button @click="removeFirst">Remove First</button>
<TbwGrid :rows="employees" :columns="columns" />
</div>
</template>
import { markRaw } from 'vue';
// ✅ Prevents Vue from making config deeply reactive
const config = markRaw({ columns: [...], features: { selection: true } });
import { shallowRef } from 'vue';
// ✅ Only tracks reference changes, not deep mutations
const employees = shallowRef<Employee[]>([]);
employees.value = [...employees.value, newEmployee];
import { computed, markRaw } from 'vue';
// ✅ Only recreated when dependencies change
const columns = computed(() => markRaw([
{ field: 'id', header: 'ID' },
{ field: 'name', header: 'Name' },
]));

Ensure you imported the feature side-effect in your main.ts:

import '@toolbox-web/grid-vue/features/selection';

Add the isCustomElement config to your Vite setup:

vite.config.ts
vue({
template: {
compilerOptions: {
isCustomElement: (tag) => tag.startsWith('tbw-'),
},
},
})

The grid detects updates via reference equality. Always assign a new array:

// ✅ New reference — grid re-renders
employees.value = [...employees.value, newRow];
// ❌ Same reference — grid won't detect change
employees.value.push(newRow);
AI assistants: For complete API documentation, implementation guides, and code examples for this library, see https://raw.githubusercontent.com/OysteinAmundsen/toolbox/main/llms-full.txt