@lilydesignsystem/vue-data-grid

Lily Design System Vue 3 headless interactive data grid (sort, filter, selection, pagination, resize, reorder, cell editing, virtualization, export, twelve cell types) over the DataTable family
335 3
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
Maintainer: joelparkerhenderson

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 text filter (both must pass).
  • Multi-column sort: thenBy (a v-model, with onThenByChange) holds further sort keys after sort. Shift+click on another sortable header adds it (ascending → descending → off); a plain click sets sort alone and clears thenBy. Each sort is read once per row per key and ties fall to the next key; missing values still sort last. Headers carry aria-sort for every key and data-sort-order (1 = primary) while more than one key is set; labels.sortAnnouncement announces the change.

Export: copy to clipboard (0.10.0)

  • Copy to clipboard: labels.copy names a button (data-…-export-format="copy"; for the kanban board, Gantt chart and calendar it also needs labels.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. copyToClipboard uses the async Clipboard API and falls back to a hidden, selected <textarea> with execCommand("copy"); it never throws. labels.copyAnnouncement(rowCount) is announced, labels.copyFailed when neither path worked, and the copy event and copyHandler({ text, rowCount }) may return false to 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)

  • validate and aria-invalid on 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-edit event 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 a readonly component or without the cell-edit event. labels.undoAnnouncement(header, text) / labels.redoAnnouncement(header, text) are announced. The stacks are the pure edit-history.ts (recordEdit, undoEdit, redoEdit, historyKey).

Defaults, locale, fields, labels and direction (0.10.0)

  • Empty values. emptyValue (grid) and a column's own emptyValue give a missing value (null, undefined or "") visible text such as "—"; Lily ships none. The cell always carries data-empty for 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 with Intl.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 own compare always wins; typed columns keep their type's comparison.
  • Field-specific search. filterField (a v-model, onFilterFieldChange) limits the text filter to one column; labels.searchField (the select's name) with labels.searchAllFields (its first option) shows the select, listing the filterable columns. Without a field the whole row is searched.
  • movableColumns makes every column movable (Alt+Arrow on the header, or drag), unless a column's movable is false.
  • 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 pure isRtl / logicalKey are exported.
  • Labels. DataGridLabels is the one bundle of every string. What each feature needs is listed in CONTROL_LABELS (labels that name a control) and ANNOUNCEMENT_LABELS (live-region messages), and requiredLabels(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 that labels leaves 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. sortAnnouncement receives 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} })}); rowClass does the same for classes, and a column's cellAttrs / cellClass for 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

@lilydesignsystem/vue-calendar

Lily Design System Vue 3 headless calendar (week, four-week and month views over...

717 3
@lilydesignsystem/vue-kanban-board

Lily Design System Vue 3 headless kanban board (pointer + keyboard-accessible ca...

0 3
@lilydesignsystem/vue-gantt-chart

Lily Design System Vue 3 headless Gantt chart (column-span task bars over GanttT...

985 3
ag-grid-vue3

AG Grid Vue 3 Component

8,744,304 15,664
ag-grid-vue

AG Grid Vue 2 Component

912,577 15,647

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