The zero-dependency JavaScript data table that turns any array, API, or CSV into an editable, filterable, mobile-ready table.
import TableCrafter from 'tablecrafter';
const table = new TableCrafter('#my-table', {
data: '/api/employees',
columns: [{ field: 'name', label: 'Name', editable: true }],
editable: true,
});
table.render();- No build step required. Drop the UMD script on any page and you are done. No bundler, no framework, no fuss.
- Spreadsheet-grade editing in the browser. 14 built-in cell editors, 15+ validation rules, lookup dropdowns, formula columns, and a custom cell-type registry -- all without a server round-trip per keypress.
- Mobile cards, not squished columns. The responsive card layout collapses rows into readable cards at configurable breakpoints instead of making users scroll sideways.
- Your data stays yours. No SaaS, no telemetry, no external requests unless you point it at your own API. MIT licensed.
| Feature | Details |
|---|---|
| Multi-column sort | Unlimited sort keys, shift-click, badges, custom comparators |
| Per-column filters | Text, multiselect, date range, number range; type auto-detection |
| Advanced search grammar | AND / OR / negation / field:value / regex / comparison operators |
| Mobile card view | Responsive breakpoints, expandable sections, field-level visibility |
| Virtual scroll | Windowed rendering via enableVirtualScroll() -- no pagination required |
| Formula columns | Arithmetic, comparisons, IF, CONCAT, LENGTH, UPPER, LOWER |
| Conditional formatting | Data bars, color scales, icon sets; ARIA labels on visual-only cues |
| Heatmap cells | Inline SVG heatmap from an array-of-numbers (cellType: 'heatmap') |
| Cell renderers | Badge, link, progress bar, sparkline built-in |
| Cell range selection | Click-drag range with TSV clipboard copy |
| Right-click context menu | ARIA-compliant, fully configurable items; keyboard navigation |
| Column management | Programmatic reorder, show/hide, pin left/right |
| RTL support | Locale-driven layout flip (dir="rtl" + tc-rtl class) |
| i18n | 6 bundled locales: en, es, fr, de, ar, ur; custom number/date formatters |
| Feature | Details |
|---|---|
| 14 inline cell editors | text, textarea, number, email, date, datetime, select, multiselect, checkbox, radio, file, url, color, range |
| Custom cell type registry | registerCellType() for any editor |
| 15+ validation rules | required, minLength, maxLength, min, max, pattern, email, url, phone, date bounds, oneOf, notOneOf, unique, custom function |
| Add row modal | Built-in creation form with full validation |
| Bulk operations | Multi-row select, delete, export, custom actions |
| Role-based permissions | Per-action (view/edit/delete/create), row-level ownOnly ownership |
| Feature | Details |
|---|---|
| Inline data | Plain JavaScript array |
| REST API | Fetch from any URL; custom auth headers; root path; CRUD write-back |
| CSV export | RFC-4180, filtered, injection-safe |
| JSON export | Serialized current dataset |
| XLSX / PDF export | Requires optional peer deps -- see Installation |
| State persistence | localStorage / sessionStorage; saves filters, sort, page |
| Plugin system | use(plugin, opts) / unuse(name) with full lifecycle hooks |
| Events API | on / off / once for 8 named events |
Credential-bearing sources (Airtable, Notion, SQL databases) must not be reached directly from the browser — their tokens and connection strings would be exposed. Put a thin server proxy in front of them: see docs/server-proxy-recipes.md for Airtable, Notion, and SQL recipes.
| Feature | Details |
|---|---|
| Zero runtime dependencies | Pure vanilla JavaScript |
| ESM + CJS + UMD builds | Works with webpack, Vite, Rollup, or a plain <script> tag |
| CDN auto-init | TableCrafter.bootstrap() from data-tc-bootstrap attributes |
| TypeScript definitions | Full .d.ts included (src/tablecrafter.d.ts) |
| Framework-agnostic | React, Vue, Svelte, Angular, or plain HTML |
| jsDelivr | https://cdn.jsdelivr.net/npm/tablecrafter@2/dist/tablecrafter.umd.min.js |
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/tablecrafter@2/dist/tablecrafter.css">
<script src="https://cdn.jsdelivr.net/npm/tablecrafter@2/dist/tablecrafter.umd.min.js"></script>The global is TableCrafter.
npm install tablecrafterimport TableCrafter from 'tablecrafter';
import 'tablecrafter/dist/tablecrafter.css';npm install xlsx # XLSX export
npm install jspdf jspdf-autotable # PDF export<div id="my-table"></div>
<script src="https://cdn.jsdelivr.net/npm/tablecrafter@2/dist/tablecrafter.umd.min.js"></script>
<script>
const table = new TableCrafter('#my-table', {
data: [
{ id: 1, name: 'Alice', role: 'Engineer' },
{ id: 2, name: 'Bob', role: 'Designer' },
],
columns: [
{ field: 'id', label: 'ID' },
{ field: 'name', label: 'Name', editable: true },
{ field: 'role', label: 'Role', editable: true },
],
editable: true,
filterable: true,
pagination: true,
});
table.render();
</script>Any element with a data-tc-bootstrap attribute is instantiated automatically when
TableCrafter.bootstrap() runs. The configuration comes from the element's
data-tc-config JSON attribute:
<div data-tc-bootstrap
data-tc-config='{"columns":[{"field":"name","label":"Name"}],
"data":[{"name":"Alice"},{"name":"Bob"}],
"filterable":true}'></div>
<script src="https://cdn.jsdelivr.net/npm/tablecrafter@2/dist/tablecrafter.umd.min.js"></script>
<script>
const tables = TableCrafter.bootstrap(); // Map<HTMLElement, TableCrafter>
// TableCrafter.bootstrap('#my-section'); // scope to a subtree
</script>The examples/ directory contains runnable HTML files. Start any static server from the repo root:
python -m http.server 8000
# then open http://localhost:8000/examples/advanced-features.htmlTableCrafter.js shares feature DNA with the TableCrafter WordPress plugin. If you need server-side data sources, Gravity Forms write-back, Gutenberg blocks, or WooCommerce integration, the plugin handles those. For the current gap status between the two, see docs/PARITY.md.
Visit tablecrafter.com for the plugin.
Constructor: new TableCrafter(container, config) where container is a CSS selector string or HTMLElement.
| Key | Type | Default | Description |
|---|---|---|---|
data |
array | string |
[] |
Row data or URL to fetch |
columns |
array |
[] |
Column definitions (required) |
editable |
boolean |
false |
Enable inline editing globally |
sortable |
boolean |
true |
Enable column sorting |
filterable |
boolean |
true |
Enable per-column filters |
globalSearch |
boolean |
true |
Enable the search grammar bar |
pagination |
boolean |
false |
Enable pagination |
pageSize |
number |
25 |
Rows per page |
exportable |
boolean |
false |
Show CSV export button |
exportFilename |
string |
'table-export.csv' |
Default download filename |
{
field: 'email', // data key (required)
label: 'Email', // header text
type: 'email', // cell renderer: text|badge|link|progress|sparkline|...
editable: true, // enable this column for editing
sortable: true,
filterable: true,
formula: 'price * qty', // computed column expression
aggregate: 'sum', // sum | count | avg | min | max | distinct
cellType: 'heatmap', // built-in cell type override
lookup: {
url: '/api/users',
valueField: 'id',
labelField: 'name',
},
}const table = new TableCrafter('#table', {
data: rows,
columns: [
{ field: 'email', label: 'Email', editable: true },
{ field: 'age', label: 'Age', editable: true },
],
validation: {
rules: {
email: [{ type: 'required' }, { type: 'email' }],
age: [{ type: 'min', value: 18 }, { type: 'max', value: 120 }],
},
},
onEdit({ row, field, value }) {
console.log(`${field} changed to`, value);
},
});
table.render();const table = new TableCrafter('#table', {
data: rows,
columns,
permissions: {
enabled: true,
edit: ['admin', 'manager'],
delete: ['admin'],
ownOnly: true, // users see only rows they own
},
});
table.setCurrentUser({ id: 42, roles: ['manager'], username: 'alice' });
table.render();Restrict an individual column's inline editor to specific roles with
editableRoles, and (optionally) show a tooltip on cells the current user
cannot edit:
const table = new TableCrafter('#table', {
data: rows,
columns: [
{ field: 'name', label: 'Name', editable: true },
{ field: 'salary', label: 'Salary', editable: true, editableRoles: ['admin'] },
],
roles: ['viewer'], // or call table.setCurrentUser({ roles: [...] })
showPermissionTooltip: true, // tooltip on restricted cells
});Advisory only.
editableRoles,permissions, and every other role check in TableCrafter.js are client-side hints. They hide UI and block the editor in the browser, but a determined user can bypass them. Your server must enforce the same restrictions independently as the source of truth.
const table = new TableCrafter('#table', {
data: rows,
columns,
i18n: {
locale: 'ar', // Arabic -- triggers RTL layout automatically
fallbackLocale: 'en',
messages: {
ar: { 'search.placeholder': 'ابحث...' },
},
},
});Six locales are bundled: en, es, fr, de, ar, ur. RTL is applied automatically for ar and ur.
Supply i18n.formats to override how number and date columns render:
i18n: {
locale: 'de',
formats: {
// Intl.NumberFormat options object, or a function(value, locale) => string
formatNumber: { style: 'currency', currency: 'EUR' },
// Function(value, locale) => string
formatDate: (value, locale) =>
new Intl.DateTimeFormat(locale, { dateStyle: 'medium' }).format(new Date(value)),
},
},table.setLocale('fr');
// Register or override message keys for any locale
table.addMessages('es', { 'toolbar.search': 'Buscar...' });const myPlugin = {
name: 'my-plugin',
install(table, opts) {
// extend or monkey-patch `table` here
console.log('installed with', opts);
},
};
const table = new TableCrafter('#table', {
data: rows,
columns,
plugins: [[myPlugin, { debug: true }]],
});Plugins declare hooks inside a hooks object. Return false from any before* hook to
cancel the operation. Available pairs: beforeRender/afterRender, beforeSort/afterSort,
beforeEdit/afterEdit (payloads: { rowIndex, field, value } / { rowIndex, field, oldValue, newValue }),
beforeLoad/afterLoad (payloads: { source } / { data }), and destroy.
const guardPlugin = {
name: 'guard',
hooks: {
beforeEdit: ({ field, value }) => {
if (field === 'salary' && value < 0) return false; // cancel
},
afterEdit: ({ field, oldValue, newValue }) => {
console.log(`${field}: ${oldValue} -> ${newValue}`);
},
destroy: () => console.log('teardown'),
},
};The events API lets you observe table activity from outside the config callbacks.
on() and once() both return an unsubscribe function.
// Persistent subscription
const unsub = table.on('cellEdit', ({ row, field, oldValue, newValue }) => {
console.log(`Row ${row}: ${field} changed from ${oldValue} to ${newValue}`);
});
// One-shot subscription (auto-removes after the first firing)
table.once('rowAdd', ({ row, index }) => {
console.log('First row added at index', index, ':', row);
});
// Unsubscribe via the returned function
unsub();
// Or by reference
const handler = ({ page }) => console.log('page', page);
table.on('pageChange', handler);
table.off('pageChange', handler);| Event | Payload | Fired by |
|---|---|---|
cellEdit |
{ row, field, oldValue, newValue } |
saveEdit() / cell commit |
selectionChange |
{ selectedRows } |
toggleRowSelection() |
sort |
{ sortKeys } |
sort() / multiSort() |
filter |
{ filters } |
setFilter() / clearFilters() |
pageChange |
{ page } |
goToPage() / nextPage() / prevPage() |
rowAdd |
{ row, index } |
addRow() |
rowUpdate |
{ row, index, previous } |
updateRow() |
rowDelete |
{ row, index } |
removeRow() |
Config callbacks (onEdit, onSort, etc.) still fire alongside events. A throwing handler is
caught and logged; other handlers in the same event still run. See examples/events-and-hooks.html.
The package ships src/tablecrafter.d.ts. The types field in package.json points to it automatically.
import TableCrafter, { TableCrafterConfig, TableCrafterColumn } from 'tablecrafter';
const columns: TableCrafterColumn[] = [
{ field: 'id', label: 'ID' },
{ field: 'name', label: 'Name', editable: true },
];
const table = new TableCrafter('#table', { data: [], columns });
table.render();import { useEffect, useRef } from 'react';
import TableCrafter from 'tablecrafter';
export function DataTable({ data, onEdit }) {
const ref = useRef(null);
useEffect(() => {
const table = new TableCrafter(ref.current, { data, onEdit, columns: [] });
table.render();
return () => table.destroy();
}, [data]);
return <div ref={ref} />;
}CSV and JSON export work out of the box:
table.downloadCSV(); // triggers browser download
const json = table.exportToJSON();XLSX and PDF export require optional peer dependencies (install commands above under Installation).
| Method | Description |
|---|---|
render() |
Render or re-render the table |
destroy() |
Tear down and remove listeners |
setData(rows) |
Replace all data |
getData() |
Current (unfiltered) data |
getFilteredData() |
Data after search/filter applied |
addRow(row) |
Append a row |
updateRow(i, updates) |
Patch a row by index |
removeRow(i) |
Delete a row by index |
setFilter(field, value) |
Set a column filter |
clearFilters() |
Reset all filters |
sort(field, opts?) |
Sort by field |
multiSort(keys) |
Sort by multiple fields |
goToPage(n) |
Jump to page |
exportToCSV() |
Return CSV string |
exportToJSON() |
Return JSON string |
downloadCSV() |
Trigger browser CSV download |
setCurrentUser(user) |
Set user context for permissions |
hasPermission(action, row?) |
Check permission |
use(plugin, opts?) |
Register a plugin |
unuse(name) |
Remove a plugin by name |
setLocale(locale) |
Switch i18n locale at runtime |
addMessages(locale, messages) |
Register or override i18n strings |
on(event, handler) |
Subscribe to a named event; returns unsub function |
off(event, handler) |
Unsubscribe a handler |
once(event, handler) |
Subscribe for one emission; returns unsub function |
enableVirtualScroll(opts?) |
Enable windowed rendering (rowHeight, viewportHeight, overscan) |
disableVirtualScroll() |
Disable virtual scroll |
isVirtualScrolling() |
Returns true when virtual scroll is active |
getAggregates() |
Aggregated column values (sum / count / avg / min / max / distinct) |
saveState() |
Persist current state |
loadState() |
Restore persisted state |
snapshotHTML(opts?) |
Deterministic HTML snapshot for testing (`scope: 'table' |
TableCrafter.bootstrap(scope?) |
Auto-init from [data-tc-bootstrap] elements; returns Map |
TableCrafter.getBrowserSupport() |
Capability probe returning { intl, resizeObserver, requiredFeaturesAvailable, ... } |
TableCrafter.minimumBrowserSupportNotice() |
Human-readable string listing minimum requirements |
npm test # run Jest suite
npm run test:watch
npm run test:coverage- Fork the repository
- Create a feature branch:
git checkout -b feature/description - Add tests for new functionality
- Ensure all tests pass:
npm test - Open a pull request against
main
Roadmap: parity gaps and planned work are tracked in Epic #323 and the parity matrix.
MIT -- see LICENSE.