@brillliand/vue-table-chad

Composable headless table primitives for Vue 3
358 2
Install
npm install @brillliand/vue-table-chad
Latest Version:0.5.1
Vue:3
License:MIT
Last Updated:Oct 19, 2026
Links: GitHub  ·  npm
Maintainer: brillliand

vue-table-chad

Home · Docs · Demo

Composable table building blocks for Vue 3 — sorting, Excel-style filters, pagination, row selection, row grouping, column layout, multi-row header bands, inline editing and a keyboard cell cursor, over local arrays or server endpoints, interchangeably.

Deliberately not a god component. Three layers, each usable on its own:

Layer What it is Use when
core/ Composables + pure functions. No components. You want the logic and none of the markup.
primitives/ Headless components. Slots, data-* attributes, no CSS. You want your own markup.
preset/ DataTable + a stylesheet, assembled from the primitives. You want a table right now.

DataTable owns no logic of its own — every capability it has comes from a primitive or a composable, and every region is a slot. When it stops fitting, drop one layer down and rebuild it differently. Nothing is hidden behind it.

Source, docs and the demo live at github.com/BrilllianD/vue-table-chad. The relative links below resolve there.

Install

npm install @brillliand/vue-table-chad   # or pnpm add / yarn add

Vue 3.5+ is the one peer dependency; the library brings no runtime dependency of its own. ESM only — there is no CJS build. TypeScript is optional but is most of the value: the column definitions are where the types earn their keep.

Four entry points, and nothing is imported that you did not ask for:

Import What it is
@brillliand/vue-table-chad Everything: DataTable, the primitives, the composables, the types.
@brillliand/vue-table-chad/style.css The preset's stylesheet. Only DataTable needs it; the primitives ship no CSS.
@brillliand/vue-table-chad/themes/<name>.css One of thirty opt-in palettes, switched on with data-vtc-theme="<name>". See Theme presets.
@brillliand/vue-table-chad/locales es, ja, ru, zhCN — complete label records, one app.use for the whole app. See Labels and i18n.

Using it in another project covers the rest: the row type the components insist on, the DataTable props and slots, and the three levels you can build a table at. Not using TypeScript? The same page without it.

Quick start

Everything below is one file; paste it in as App.vue and it runs.

<script setup lang="ts">
import { shallowRef } from 'vue'
import { DataTable, useLocalDataSource, useTableState, type ColumnDef } from '@brillliand/vue-table-chad'
import '@brillliand/vue-table-chad/style.css'

type Person = { id: number; name: string; department: string; salary: number; hiredAt: string }

// Your rows. Anything with an `id` works; see "Hold rows in a shallowRef" below.
const rows = shallowRef<Person[]>([
  { id: 1, name: 'Ada Lovelace', department: 'Engineering', salary: 120_000, hiredAt: '2019-03-04' },
  { id: 2, name: 'Grace Hopper', department: 'Engineering', salary: 145_000, hiredAt: '2017-08-15' },
  { id: 3, name: 'Barbara Liskov', department: 'Research', salary: 150_000, hiredAt: '2021-01-11' },
])

const columns: ColumnDef<Person>[] = [
  { id: 'name', header: 'Name', type: 'text', pinned: 'left' },
  { id: 'department', header: 'Department', type: 'enum', options: ['Engineering', 'Research'] },
  { id: 'salary', header: 'Salary', type: 'number', align: 'right',
    format: (v) => (v == null ? '—' : `$${Number(v).toLocaleString()}`) },
  { id: 'hiredAt', header: 'Hired', type: 'date' },
]

const state = useTableState({ pageSize: 25 })
const source = useLocalDataSource(rows, columns, state.query)
</script>

<template>
  <DataTable :columns="columns" :source="source" :state="state" selectable>
    <template #cell:name="{ row }"><a :href="https://github.com/BrilllianD/vue-table-chad/blob/HEAD/`/people/${row.id}`">{{ row.name }}</a></template>
  </DataTable>
</template>

type is what makes filters and sorting behave: it picks the comparator and decides which operators the filter panel offers (contains for text, between for numbers, before/after for dates).

Rows are identified by row.id. Pass getRowId when they are keyed by something else — it drives selection and the render keys, so editing a row patches it in place instead of replacing it.

Hold rows in a shallowRef

shallowRef, not ref, and it is the single highest-leverage line in that example.

ref(people) deep-proxies the array and every object in it. Filtering, sorting, grouping and aggregating then read each cell through a Proxy trap — once per row per column per pass, which at 10k rows and ten columns is 100 000 trap hits for one keystroke. A shallowRef proxies the array reference alone and hands the pipeline the raw objects.

You give up nothing the table uses. It never mutates a row, and it re-runs whenever the ref is reassigned:

rows.value = [...rows.value, newPerson]   // seen
rows.value.push(newPerson)                // not seen — call source.refresh()

source.refresh() exists for exactly that case: mutate the array in place, then say so.

If rows are large and you never reassign individual ones, markRaw on each row opts them out of reactivity permanently, which also stops a cell slot from accidentally making one reactive later.

Docs

brillliand.github.io/vue-table-chad — the docs site, with the live demo beside it.

Each page is one topic, and each has a matching view in npm run demo where the same thing runs. The pages are also a VitePress site — npm run docs:dev serves it locally, npm run docs:build builds it.

Page What it covers See it live
Using it in another project Installing it, the row type it insists on, the DataTable props and slots, and the three levels you can build a table at. Everything at once
…from plain JavaScript The same, without TypeScript: what a column accepts, the four mistakes the compiler would have caught, and how to get autocomplete back with JSDoc. Everything at once
The two contracts QueryState and DataSource — the two interfaces everything else is written against. Core only
Local, server and infinite data useLocalDataSource, useServerDataSource, useInfiniteDataSource, and why swapping one for another changes nothing above. Server data, Infinite scroll
Excel-style filters The value checklist, condition rules, and facets. Filters
Sorting and pagination usePagination, PageItem, SortOptions, per-column comparators, and null-sorting. Everything at once
Selection Shift-ranges, the tri-state header, and selecting more rows than are loaded. Selection
Grouping rows Bands, groupMode, aggregates and whole-table totals. Grouping
Expandable detail rows A row opens to show the entities that hang off it — expandable, the #detail slot, useRowExpansion, and what virtual needs. Detail rows
The right-click menu Five actions on the cell under the pointer, Shift+F10 from the keyboard, and the #contextMenu slot for items of your own. Right-click menu
Editing cells A draft per row, cell and row mode, validation, and a save the server can refuse. Editing
Keyboard navigation The cell cursor: arrow keys, Enter or any character to edit, copy and paste, and the roving tabindex behind it. Cell cursor
Column layout Visibility, order, widths, pinning, persistence and drag-to-reorder. Column layout
Header bands Multi-row headers: banding columns under a shared header, nesting them, and folding a band shut. Header bands
Styling The --vtc-* variables, striping, and how cell backgrounds stack. Theming
Theme presets Thirty named palettes as opt-in stylesheets, switched by one attribute — and why a palette is only nine colours. Themes
Labels and i18n The four shipped locales, one app.use for the whole app, overriding every string the table renders, and the spec that keeps new literals out. Labels
Virtual rows Windowing a fixed-height list, and the whole-result-set caveat over a server source. Virtual rows
Composing your own Building a different table from the same parts, and hoisting state into a URL or store. Composed, Hoisted state
Performance The shallowRef rule, the invalidation invariants, and the benchmark numbers behind them. Performance
Recipes Six worked recipes, from a bare table to retheming without touching a component. Recipes

Also: Porting an existing table — a real server-paginated Options API component moved onto useTableState + useServerDataSource, keeping its own markup, mixins and widgets.

The full API — every export, with what it is for — is the API reference tab of the demo (npm run demo in a checkout). It is generated from the doc comments in src/, so it cannot fall behind the code.

Not included

Tree rows (parent/child hierarchies, as opposed to the value-based grouping in Grouping rows), pinned rows, and pivoting. Aggregation covers sum/avg/min/max and no custom reducer: the reducer shape that lands will be an incremental one, because a whole-array (rows) => value callback would mean materializing per-group row arrays and a second pass over the dataset.

Selection is per row, not per cell range. There is a cell cursor, and it copies and pastes a single cell — see Keyboard navigation — but Shift+arrow does not extend a block, and block paste and fill-down are both waiting on that range rather than on themselves. Editing keeps no undo stack: a save goes through the data source, so the server stays the record. CSV/TSV export of the whole result set is in, see Local, server and infinite data.

Virtualization is rows only — see Virtual rows. Rows are taken to be one height unless measure-rows is on, which measures each rendered row at the cost of a layout per update. It slotted in at the rendering layer as a page size of everything, so there is still exactly one list for grouping, selection and the cursor to read. Columns are not windowed; 34 of them show no need today, and a bench decides that rather than a design. TASKS.md carries the rest, and why each of these is a decision rather than an oversight.

Developing

Node 24 (.nvmrc is committed — run nvm use) and npm.

nvm use             # Node 24
npm install
npm run dev         # playground at http://localhost:5173
npm run demo        # full feature demo at http://localhost:5174
npm test            # vitest
npm run typecheck
npm run lint
npm run bench       # pipeline and interaction benchmarks
npm run build       # library -> dist/
npm run size        # bundle-size budget over dist/
npm run docs:dev    # the VitePress docs site, locally
npm run build:site  # docs + demo, as published -> docs/.vitepress/dist/
npm run build:docs  # the demo, folded into one self-contained page
make pack           # build + size check + npm pack -> the installable tarball

npm run dev is four short examples. npm run demo is the exhaustive one — 23 views, every export, one view per feature area, each listing the API it uses. See demo/README.md. Inside this repo, @brillliand/vue-table-chad is an alias onto src/index.ts.

Also in the repo: bench/BASELINE.md for the benchmark numbers and the optimizations that turned out not to be worth it, TASKS.md for what is planned and what is deferred, CLAUDE.md for the layer contracts and performance invariants, and RELEASING.md for the release checklist.

License

MIT.

Related Packages

@kikiloaw/simple-table

A lightweight, dependency-light DataTable component for Vue 3 with Tailwind CSS

2,554 1
gridvuejs

A reusable Vue 3 data grid component with filtering, sorting, grouping, and pagi...

0
@dolusoft/query-table

A thin Vue 3 table for server-side data: it renders rows and emits sort, filter,...

1,825 1
@querry-kit/nuxt-ui

Customizable Nuxt UI table components for Query Kit

3,466 0
@vue-data-grid/engine

Headless table core for Vue: column model, row and column windows, CSS-variable...

229

Version History

Version Released Vue License
0.5.1 ^3.5.0 MIT
0.5.0 ^3.5.0 MIT