@lilydesignsystem/vue-data-grid
| Install | |
|---|---|
npm install @lilydesignsystem/vue-data-grid |
|
| Latest Version: | 0.11.0 |
| Vue: | 3 |
| License: | MIT OR Apache-2.0 OR GPL-2.0-only OR GPL-3.0-only OR BSD-3-Clause |
| Last Updated: | Oct 25, 2026 |
| Links: | GitHub · npm |
DataGrid (Vue helper)
A reusable Vue 3 headless interactive data grid. It composes
@lilydesignsystem/vue-headless's DataTable family — the same role="grid" container the headless catalog
ships, with zero behaviour built in — and adds sort, client-side filter, row selection, column
resize / visibility / reorder / pinning, pagination, WAI-ARIA APG Grid keyboard navigation,
inline cell editing, row virtualization, export to CSV / TSV / Excel and twelve cell types
(text, number, link, date, time, time with time zone, select, radio, person, file, rollup,
formula). Ships no CSS: every visual detail is the consumer's, via kebab-case class hooks.
Ported from the canonical @lilydesignsystem/svelte-data-grid,
whose spec is the contract.
Usage
<script setup lang="ts">
import { h, ref } from "vue";
import DataGrid from "@lilydesignsystem/vue-data-grid";
import type { DataGridColumn } from "@lilydesignsystem/vue-data-grid";
const columns: DataGridColumn[] = [
{ id: "name", header: "Name", sortable: true, resizable: true },
{ id: "email", header: "Email", sortable: true, cell: ({ value, formatted }) => h("a", { href: `mailto:${value}` }, formatted) },
{ id: "role", header: "Role", sortable: true, hidable: true },
];
const rows = [
{ name: "Alice", email: "alice@example.com", role: "Admin" },
{ name: "Bob", email: "bob@example.com", role: "Editor" },
];
const selected = ref<string[]>([]);
const labels = { search: "Search users", selectAll: "Select all rows", selectRow: (name: string) => `Select ${name}` };
</script>
<template>
<DataGrid label="Users" :columns="columns" :rows="rows" selection-mode="multiple" v-model:selected="selected" :labels="labels" />
</template>
What is Vue-specific
| Svelte | Vue |
|---|---|
selected, filter, sort, columnOrder, page (bindable) |
v-model:selected, v-model:filter, v-model:sort, v-model:columnOrder, v-model:page |
onSelectionChange, onFilterChange, onSortChange, onColumnOrderChange |
the matching update: events of those models |
onCellEdit, onFilesSelected, onFileRemove, onRowActivate |
the events cell-edit, files-selected, file-remove, row-activate (Enter on a cell activates its row only when a row-activate listener is attached) |
onExport (return false to stop the save) |
the exportHandler prop (return false) and the export-file event |
empty: Snippet, footer: Snippet<[{ columns, rows }]> |
the empty slot and the footer slot (props columns, rows) |
column cell, headerCell, avatar, personEditor snippets |
functions returning a vnode (h()) |
class |
class (falls through to the root <div>) |
Everything else — columns, labels, rowId, selectionMode, pageSize, storageKey, mode,
the keyboard model, the cell types, editing, export and virtualization — is as in the Svelte
reference. Every user-facing string is a labels.* prop (a label's presence gates the control it
names); nothing is hardcoded.
Install
npm install @lilydesignsystem/vue-data-grid
Requires @lilydesignsystem/vue-headless.
Filtering and sorting (0.10.0)
rowFilter(row)hides rows it rejects, combining with the textfilter(both must pass).- Multi-column sort:
thenBy(av-model, withonThenByChange) holds further sort keys aftersort. Shift+click on another sortable header adds it (ascending → descending → off); a plain click setssortalone and clearsthenBy. Each sort is read once per row per key and ties fall to the next key; missing values still sort last. Headers carryaria-sortfor every key anddata-sort-order(1 = primary) while more than one key is set;labels.sortAnnouncementannounces the change.
Export: copy to clipboard (0.10.0)
- Copy to clipboard:
labels.copynames a button (data-…-export-format="copy"; for the kanban board, Gantt chart and calendar it also needslabels.exportHeaders, like the downloads) that puts exactly what a download would hold — the same rows, scope and order, header row first — on the clipboard as tab-separated text (toClipboardText: no byte-order mark, formula text defused), ready to paste into a spreadsheet.copyToClipboarduses the async Clipboard API and falls back to a hidden, selected<textarea>withexecCommand("copy"); it never throws.labels.copyAnnouncement(rowCount)is announced,labels.copyFailedwhen neither path worked, and thecopyevent andcopyHandler({ text, rowCount })may returnfalseto take over (nothing is copied).
Keyboard: page keys and type-ahead (0.10.0)
- Type-ahead: letters typed in quick succession (within 700 ms) move the cursor to the next row whose cell text, in the column the cursor is in, starts with them (
findTypeaheadMatch; the text is what the cell shows, ignoring case and diacritics). The same letter pressed again steps through the rows that start with it; a Ctrl, Alt or Meta key, and a prefix that matches nothing, do nothing. PageUp / PageDown already moved a page.
In-place edit: validation, undo and redo (0.10.0)
validateandaria-invalidon a refused cell were already there; undo and redo are new.- Undo and redo: every accepted edit is remembered (the latest 100). Ctrl/Cmd+Z puts the earlier value back and Ctrl/Cmd+Shift+Z (or Ctrl+Y) re-applies one, both through the
cell-editevent with the restored value and the one it replaced — the consumer owns the data, so an undo is reported like an edit, and an undo is not itself recorded. A new edit ends the redo chain. Inside a field editor the browser's own undo is left alone; nothing happens in areadonlycomponent or without thecell-editevent.labels.undoAnnouncement(header, text)/labels.redoAnnouncement(header, text)are announced. The stacks are the pureedit-history.ts(recordEdit,undoEdit,redoEdit,historyKey).
Defaults, locale, fields, labels and direction (0.10.0)
- Empty values.
emptyValue(grid) and a column's ownemptyValuegive a missing value (null,undefinedor"") visible text such as"—"; Lily ships none. The cell always carriesdata-emptyfor CSS, and an export is unaffected.nulls("last"default, or"first") says where missing values sort, in both directions. - Locale-aware sorting. With a
locale(or a column's own), plain text sorts withIntl.Collator(locale, { numeric: true })— "Item 2" before "Item 10", an accented letter with its base letter. Without a locale text sorts by code point, as before. A column's owncomparealways wins; typed columns keep their type's comparison. - Field-specific search.
filterField(av-model,onFilterFieldChange) limits the text filter to one column;labels.searchField(the select's name) withlabels.searchAllFields(its first option) shows the select, listing the filterable columns. Without a field the whole row is searched. movableColumnsmakes every column movable (Alt+Arrow on the header, or drag), unless a column'smovableisfalse.- Right-to-left. The horizontal arrows are visual, so in a right-to-left layout (the nearest
dir, else the computed direction) ArrowLeft goes to the next cell and ArrowRight to the previous, and Alt+Arrow moves a column the way the arrow points. Vertical keys, Home, End and the page keys do not change. The pureisRtl/logicalKeyare exported. - Labels.
DataGridLabelsis the one bundle of every string. What each feature needs is listed inCONTROL_LABELS(labels that name a control) andANNOUNCEMENT_LABELS(live-region messages), andrequiredLabels(features)/missingLabels(features, labels)compute it, so an app can assert in a test that its bundle for a locale is complete. In development the grid logs once, naming the labels its features call for thatlabelsleaves out (warnMissingLabels={false}silences it; it never logs in production). Lily has no English fallback: a missing label is a control with no name or an announcement never made.sortAnnouncementreceives the ARIA token (ascending,descending,none), which the bundle turns into its own words. - Row attributes.
rowAttrs(row)returns attributes spread onto each body row — the way to put test ids (data-testid), data attributes and other hooks on rows (rowAttrs={(row) => ({ "data-testid":row-${row.id}})});rowClassdoes the same for classes, and a column'scellAttrs/cellClassfor cells.
Pager landmark (0.10.0)
labels.pagination names the pager: it then renders as a <nav> landmark (the headless PaginationNav, keeping the data-grid-pagination class) instead of a plain <div>, so a screen-reader user can jump to it. Optional — without it nothing changes.
Search (0.10.0)
search (a v-model; the field renders when labels.search names it) narrows the rows: every word must appear, in any order, ignoring case and diacritics. Every word must appear somewhere in the row (any order, any filterable cells), ignoring case and diacritics; filterText still sets a column's text. No match highlighting: cells are rendered by their cell type, so the grid marks nothing inside them. See spec §18.
Related Packages
Lily Design System Vue 3 headless calendar (week, four-week and month views over...
Lily Design System Vue 3 headless kanban board (pointer + keyboard-accessible ca...
Lily Design System Vue 3 headless Gantt chart (column-span task bars over GanttT...
Version History
| Version | Released | Vue | License |
|---|---|---|---|
| 0.11.0 | ^3.0.0 | MIT OR Apache-2.0 OR GPL-2.0-only OR GPL-3.0-only OR BSD-3-Clause | |
| 0.10.0 | ^3.0.0 | MIT OR Apache-2.0 OR GPL-2.0-only OR GPL-3.0-only OR BSD-3-Clause | |
| 0.9.0 | ^3.0.0 | MIT OR Apache-2.0 OR GPL-2.0-only OR GPL-3.0-only OR BSD-3-Clause |
Pre-releases (1)
| Version | Released | License |
|---|---|---|
| 0.0.0-stage |