TanStack Table is a headless table library. It manages your table's state and logic (sorting, filtering, pagination, selection, and more) while you keep 100% control over the markup and styles. This page gets you from install to a rendering Alpine table, then shows how to layer on your first feature.
npm install @tanstack/alpine-table alpinejsThe @tanstack/alpine-table package works with Alpine 3.
The adapter is built around two ideas:
Important
Alpine does not initialize directives (@click, x-model, etc.) inside content set with x-html. So render cell/header content with x-html="table.FlexRender(...)", but put any interactivity (click-to-sort, filter inputs, checkboxes) on real elements in your markup, next to the x-html span. You will see this pattern in the sorting example below.
You define the table in a JavaScript module with Alpine.data, then render it from your HTML.
// main.ts
import Alpine from 'alpinejs'
import { FlexRender, createTable, tableFeatures } from '@tanstack/alpine-table'
import type { ColumnDef } from '@tanstack/alpine-table'
// 1. Define the shape of your data
type Person = {
firstName: string
lastName: string
age: number
}
const defaultData: Array<Person> = [
{ firstName: 'tanner', lastName: 'linsley', age: 24 },
{ firstName: 'tandy', lastName: 'miller', age: 40 },
{ firstName: 'joe', lastName: 'dirte', age: 45 },
]
// 2. New in v9: declare which features this table uses (none yet)
const features = tableFeatures({})
// 3. Define your columns. Renderers return HTML strings (rendered via x-html).
const columns: Array<ColumnDef<typeof features, Person>> = [
{
accessorKey: 'firstName', // accessorKey shorthand
header: 'First Name',
cell: (info) => info.getValue(),
},
{
accessorFn: (row) => row.lastName, // accessorFn alternative with a custom id
id: 'lastName',
header: () => '<span>Last Name</span>',
cell: (info) => `<i>${info.getValue<string>()}</i>`,
},
{
accessorKey: 'age',
header: () => 'Age',
},
]
// 4. Register an Alpine component
Alpine.data('table', () => {
// Store data in Alpine-reactive state so updates flow into the table
const local = Alpine.reactive({ data: defaultData })
// 5. Create the table instance, reading data through a getter so it stays reactive
const table = createTable({
features,
columns,
get data() {
return local.data
},
})
// Expose the instance (and FlexRender) to the template
return { table, FlexRender }
})
window.Alpine = Alpine
Alpine.start()<!-- index.html -->
<div x-data="table">
<table>
<thead>
<template
x-for="headerGroup in table.getHeaderGroups()"
:key="headerGroup.id"
>
<tr>
<template x-for="header in headerGroup.headers" :key="header.id">
<th>
<template x-if="!header.isPlaceholder">
<span x-html="FlexRender({ header })"></span>
</template>
</th>
</template>
</tr>
</template>
</thead>
<tbody>
<template x-for="row in table.getRowModel().rows" :key="row.id">
<tr>
<template x-for="cell in row.getAllCells()" :key="cell.id">
<td x-html="FlexRender({ cell })"></td>
</template>
</tr>
</template>
</tbody>
</table>
</div>
<script type="module" src="/main.ts"></script>A few things to note:
See the full Basic createTable example for a runnable version with more columns and a footer.
Features are opt-in in v9. To make columns sortable, register rowSortingFeature and the sortedRowModel factory in tableFeatures, then wire a header click handler. Because the click handler cannot live inside x-html, wrap the rendered header in a real element and attach @click there.
// main.ts (additions)
import {
FlexRender,
createSortedRowModel,
createTable,
rowSortingFeature,
sortFns,
tableFeatures,
} from '@tanstack/alpine-table'
const features = tableFeatures({
rowSortingFeature, // enables sorting APIs and state
sortedRowModel: createSortedRowModel(), // client-side sorting
sortFns,
})
// columns and the Alpine.data registration are otherwise unchanged<!-- index.html: the <thead> becomes -->
<thead>
<template
x-for="headerGroup in table.getHeaderGroups()"
:key="headerGroup.id"
>
<tr>
<template x-for="header in headerGroup.headers" :key="header.id">
<th>
<template x-if="!header.isPlaceholder">
<div
:style="header.column.getCanSort() ? 'cursor: pointer' : ''"
@click="header.column.getToggleSortingHandler()?.($event)"
>
<span x-html="FlexRender({ header })"></span
><span
x-text="({ asc: ' 🔼', desc: ' 🔽' })[header.column.getIsSorted()] ?? ''"
></span>
</div>
</template>
</th>
</template>
</tr>
</template>
</thead>Clicking a header now toggles between ascending, descending, and unsorted. Every other feature follows this same pattern: register the feature (and its row model factory as a slot on tableFeatures if it has one), then use the APIs it adds to the table, columns, and rows, attaching any interactive controls to real elements in your markup. See the Sorting example for custom sort functions, multi-sorting, and per-column options.
Table state. In v9, table state is backed by TanStack Store atoms, which the adapter makes reactive in Alpine. You usually do not need to manage it yourself: set initialState for starting values and call feature APIs like table.setSorting(...) or table.nextPage(). When you need to read a state slice in your markup, use table.atoms.<slice>.get() (for example table.atoms.pagination.get().pageIndex) or table.store.get() for the whole state. There is no state selector, because the table instance is already reactive. The Table State Guide is the foundational guide for everything else.
Feature examples. Each feature has a runnable example, such as Column Filters, Pagination, Row Selection, and Column Visibility.
Composable tables. When multiple tables in your app share features and row models, define them once with createTableHook:
const features = tableFeatures({
rowSortingFeature,
sortedRowModel: createSortedRowModel(),
sortFns,
})
const { createAppTable, createAppColumnHelper } = createTableHook({ features })Then call createAppTable({ columns, data }) from your component instead of createTable, and define columns with createAppColumnHelper. See the Basic createAppTable example for the full pattern.
Examples. Browse the runnable Alpine examples, from basic tables to feature demos, to see intended usage end to end.