@brillliand/vue-table-chad
| Install | |
|---|---|
npm install @brillliand/vue-table-chad |
|
| Latest Version: | 0.5.1 |
| Vue: | 3 |
| License: | MIT |
| Last Updated: | Oct 19, 2026 |
| Links: | GitHub · npm |
vue-table-chad
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
A lightweight, dependency-light DataTable component for Vue 3 with Tailwind CSS
A thin Vue 3 table for server-side data: it renders rows and emits sort, filter,...
Headless table core for Vue: column model, row and column windows, CSS-variable...
Version History
| Version | Released | Vue | License |
|---|---|---|---|
| 0.5.1 | ^3.5.0 | MIT | |
| 0.5.0 | ^3.5.0 | MIT |