A lightweight, framework-agnostic data table library built with TypeScript. TableKit has zero runtime dependencies and is designed around a plugin architecture — use only what you need.
- Zero dependencies, plain TypeScript/DOM
- Plugin-based: filter, pagination, selection, column visibility
- Multiple data sources: static arrays, AJAX, async functions
- Multi-column sorting (click + Shift-click)
- Custom cell renderers and sort comparators
- CSS custom property theming
- Full TypeScript types
bun add @tablekit/core
# or
npm install @tablekit/coreimport { createTable, withFilter, withPagination, withSelection } from '@tablekit/core'
import '@tablekit/core/dist/tablekit.css'
const table = createTable('#my-table', {
columns: [
{ key: 'name', title: 'Name', type: 'string', sortable: true },
{ key: 'email', title: 'Email', type: 'string', sortable: true },
{ key: 'score', title: 'Score', type: 'number', sortable: true },
],
data: [
{ name: 'Alice', email: 'alice@example.com', score: 95 },
{ name: 'Bob', email: 'bob@example.com', score: 82 },
],
plugins: [
withFilter({ debounce: 200 }),
withPagination({ pageSize: 25, pageSizes: [10, 25, 50], showInfo: true }),
withSelection({ mode: 'multi' }),
],
})TableKit accepts three data source formats:
// Static array
data: [{ id: 1, name: 'Alice' }]
// AJAX
data: {
url: '/api/users',
method: 'GET',
headers: { Authorization: 'Bearer token' },
dataSrc: (response) => response.data, // transform response
}
// Async function
data: async () => {
const res = await fetch('/api/users')
return res.json()
}interface ColumnDef {
key: string
title: string
type?: 'string' | 'number' | 'date' | 'boolean' | 'custom'
width?: number | string
minWidth?: number
maxWidth?: number
visible?: boolean
sortable?: boolean
filterable?: boolean
resizable?: boolean
pinned?: 'left' | 'right' | false
className?: string
// Custom cell renderer — return an HTMLElement or HTML string
render?: (value: CellValue, row: Row, col: ColumnDef) => HTMLElement | string
// Custom sort comparator
compare?: (a: CellValue, b: CellValue) => number
}{
key: 'status',
title: 'Status',
render: (val) => {
const badge = document.createElement('span')
badge.className = `badge badge--${val}`
badge.textContent = String(val)
return badge
},
}Adds a global search input above the table.
| Option | Type | Default | Description |
|---|---|---|---|
debounce |
number |
250 |
Input debounce in milliseconds |
placeholder |
string |
'Search…' |
Input placeholder text |
withFilter({ debounce: 200, placeholder: 'Search users…' })Adds a pagination bar below the table.
| Option | Type | Default | Description |
|---|---|---|---|
pageSize |
number |
25 |
Initial rows per page |
pageSizes |
number[] |
— | Show a rows-per-page dropdown |
showInfo |
boolean |
true |
Show "1–25 of 120" info text |
withPagination({ pageSize: 15, pageSizes: [10, 15, 25, 50], showInfo: true })Enables row selection.
| Option | Type | Default | Description |
|---|---|---|---|
mode |
SelectionMode |
'multi' |
'single', 'multi', 'cell', 'column', or false |
checkboxColumn |
boolean |
false |
Reserved — checkbox column support |
Multi-select: plain click selects a single row; Ctrl/Cmd or Shift click toggles/adds rows.
withSelection({ mode: 'multi' })Adds a "Columns" dropdown to show/hide columns.
| Option | Type | Default | Description |
|---|---|---|---|
showToggle |
boolean |
true |
Render the toggle button |
withColumnVisibility({ showToggle: true })Subscribe to table events with table.on(). The unsubscribe function is returned.
const unsub = table.on('selection:change', ({ selection }) => {
console.log('Selected rows:', [...selection.rows])
})
// Later:
unsub()| Event | Payload |
|---|---|
data:loading |
{} |
data:loaded |
{ rows: Row[] } |
sort:change |
{ sort: SortEntry[] } |
filter:change |
{ filter: FilterState } |
page:change |
{ pagination: PaginationState } |
selection:change |
{ selection: SelectionState } |
column:visibility |
{ key: string; visible: boolean } |
column:resize |
{ key: string; width: number } |
column:reorder |
{ from: number; to: number } |
row:reorder |
{ from: number; to: number } |
render:complete |
{} |
destroy |
{} |
const table = createTable(container, options)
table.state // Readonly current state
table.render() // Force re-render
table.setState(patch) // Patch state and re-render manually
table.destroy() // Tear down table and all plugins
table.getContainer() // Returns the wrapper HTMLElement
table.getTableEl() // Returns the <table> HTMLTableElement
table.on(event, fn) // Subscribe to an event, returns unsubscribe fn
table.off(event, fn) // Unsubscribe from an event
table.emit(event, payload) // Emit an eventTableKit uses CSS custom properties. Override them on the container or any ancestor:
#my-table {
--tk-color-accent: #8b5cf6;
--tk-color-accent-hover: #7c3aed;
--tk-color-accent-light: #ede9fe;
--tk-color-bg-selected: #f5f3ff;
}A plugin is any object that implements TablePlugin:
import type { TablePlugin, TableCore } from '@tablekit/core'
function withMyPlugin(): TablePlugin {
let table: TableCore
return {
name: 'my-plugin',
install(t) {
table = t
t.on('data:loaded', ({ rows }) => {
console.log(`Loaded ${rows.length} rows`)
})
},
destroy() {
// cleanup
},
}
}Pass it in the plugins array:
createTable('#table', {
columns: [...],
data: [...],
plugins: [withMyPlugin()],
})# Install dependencies
bun install
# Start dev server with playground
bun run dev
# Build library
bun run build
# Type check
bun run typecheckThe playground is available at playground/index.html and demonstrates all built-in plugins together.
GPL-3.0