
Headless table engine with a declarative UI adapter mirroring TanStack Table v8: data modeling, sorting, filtering, grouping, pagination, selection, column features, and a flexible grid renderer.
A headless table engine for Kotlin Multiplatform with a Compose Multiplatform adapter — the data, sorting, filtering, grouping, pagination, and selection logic of TanStack Table v8, rendered through your Compose UI on Android and iOS from a single shared source set.
The same :sample Compose Multiplatform code on Android (left) and iOS (right), 2× playback speed. Full-quality MP4: Android · iOS.
val table = rememberTable(
TableOptions(
data = people,
columns = columns,
getCoreRowModel = getCoreRowModel(),
getSortedRowModel = getSortedRowModel(),
),
)
TableGrid(Modifier.horizontalScroll(rememberScrollState())) {
table.getHeaderGroups().forEach { hg ->
row {
hg.headers.forEach { h ->
cell { TableCellText(flexRender(h.column.columnDef.header, h.getContext()), bold = true) }
}
}
}
table.getRowModel().rows.forEach { r ->
row {
r.getVisibleCells().forEach { c ->
cell { TableCellText(flexRender(c.column.columnDef.cell, c.getContext())) }
}
}
}
}The engine is design-free — what you render is up to you. TableGrid is the
adapter's drop-in SubcomposeLayout for the common case (content-sized
columns, optional column-span cells, IME-safe nested text fields); skip it and
draw cells yourself if you need a different layout.
TanStack Table is the de-facto headless table engine on the web. There was no equivalent for Kotlin Multiplatform / Compose Multiplatform: existing Compose table libraries bake their own opinion into the engine and rendering, leaving you to either accept their choices or rebuild from scratch. This project ports the engine 1:1 from TanStack's TypeScript source, layers a Compose Multiplatform adapter on top, and ships them as separate Gradle modules so you take only what you need.
| Module | Targets |
|---|---|
:table-core |
commonMain (JVM 11, Android, iosX64, iosArm64, iosSimulatorArm64) |
:table-compose |
commonMain (JVM, Android, all three iOS targets) |
Compose Multiplatform 1.7.x. Kotlin 2.x. Android minSdk 24, compileSdk 35.
0.1.0 is built with Kotlin 2.1.0 and Compose Multiplatform 1.7.3.
.klibs are tied to the
Kotlin compiler version. For the iOS targets, keep your project's Kotlin
version aligned with 2.1.x — a significantly different Kotlin version can
fail to link against the native artifacts. This is a general Kotlin
Multiplatform constraint, not specific to this library.:table-core — the engine. No UI dependencies. Mirrors the TanStack
Table v8 API: Table, ColumnDef, createColumnHelper, getCoreRowModel,
getSortedRowModel, getFilteredRowModel, getGroupedRowModel,
getExpandedRowModel, getPaginationRowModel, getFacetedRowModel, and
the column / row / cell feature mixins (visibility, ordering, pinning,
sizing, filtering, sorting, grouping, expansion, pagination, selection).:table-compose — the Compose Multiplatform adapter. Provides
rememberTable (subscribes the composition to the engine's state),
flexRender (resolves cell / header / footer templates), and
TableGrid (a theme-neutral SubcomposeLayout grid).:sample — a Compose Multiplatform demo app covering 13 feature
screens. Runs on Android and iOS from one common source set.Available on Maven Central. Add mavenCentral() to your repositories, then the
dependencies to your shared module:
// settings.gradle.kts
dependencyResolutionManagement {
repositories {
mavenCentral()
}
}
// build.gradle.kts (your KMP module)
kotlin {
sourceSets {
commonMain.dependencies {
// The Compose Multiplatform adapter. Pulls in table-core transitively
// (it api-depends on the engine), so this one line is enough for most apps.
implementation("io.github.praveenshharma:table-compose:0.1.0")
// Or depend on just the headless engine — no Compose, no UI:
// implementation("io.github.praveenshharma:table-core:0.1.0")
}
}
}The API mirrors TanStack Table v8 — the TanStack Table docs
remain the canonical reference for engine concepts (row models, column
definitions, state). The differences live in Kotlin's syntax and a handful of
type-system adaptations documented in docs/INTERNALS.md.
Kotlin API reference (generated by Dokka, regenerated on every push to main):
https://praveenshharma.github.io/tanstack-table-kmp/.
A complete walkthrough for each feature lives in the :sample app — open the
example screen for the feature you want and follow the column definitions
through the rememberTable call into the TableGrid builder.
Cell composables that take a single stable parameter (a Row, Column,
TableColumn, or Header) are skipped by Compose's stability-inference
restart logic when the surrounding table state changes but the parameter
reference does not. Annotate your cell composables @NonRestartableComposable
to opt out of the skip and re-run them on every parent recomposition. The
adapter's own cell composables (TableGrid and friends) already do this;
your own cell composables need the annotation if they read engine state.
@Composable
@NonRestartableComposable
fun StatusCell(row: Row<Person>) {
val isSelected = row.getIsSelected()
// ...
}Background on why this is necessary lives in
docs/INTERNALS.md under "Compose adapter".
Each of the 13 example screens in :sample is self-contained: open the file,
follow the column definitions, the rememberTable call, and the TableGrid
builder. The sample app runs on Android (./gradlew :sample:installDebug)
and iOS (see docs/INTERNALS.md for the simulator
setup).
| Screen | Demonstrates |
|---|---|
| Basic | Core column rendering, footers |
| Sorting | Single + multi-column sort, custom sort fns |
| Filters | Per-column text filtering |
| Filters (Faceted) | Faceted unique-value / min-max filtering |
| Pagination | Page size, page navigation, manual vs auto |
| Grouping | Group rows by column, aggregation, leaf-row counts |
| Expanding | Sub-row expansion, row-level expand state |
| Row Selection | Single / multi-row selection, indeterminate header state |
| Row Pinning | Pin rows to the top or bottom |
| Column Pinning | Pin columns to left / right |
| Column Ordering | Reorder columns at runtime |
| Column Visibility | Show / hide individual columns |
| Column Groups | Nested header groups (multi-row headers) |
table-core/ — headless engine (Kotlin Multiplatform)
table-compose/ — Compose Multiplatform adapter
sample/ — demo app (Android + iOS)
iosApp/ — minimal Swift host for the iOS simulator demo
docs/INTERNALS.md — design notes and implementation invariants
See CONTRIBUTING.md. PRs are welcome; bug reports and
reproduction projects in particular. For substantial changes please open an
issue first to discuss direction.
MIT. This project is a community-maintained Kotlin Multiplatform
port of TanStack Table (also MIT,
© Tanner Linsley); see NOTICE for attribution. It is not
affiliated with, endorsed by, or sponsored by TanStack.
A headless table engine for Kotlin Multiplatform with a Compose Multiplatform adapter — the data, sorting, filtering, grouping, pagination, and selection logic of TanStack Table v8, rendered through your Compose UI on Android and iOS from a single shared source set.
The same :sample Compose Multiplatform code on Android (left) and iOS (right), 2× playback speed. Full-quality MP4: Android · iOS.
val table = rememberTable(
TableOptions(
data = people,
columns = columns,
getCoreRowModel = getCoreRowModel(),
getSortedRowModel = getSortedRowModel(),
),
)
TableGrid(Modifier.horizontalScroll(rememberScrollState())) {
table.getHeaderGroups().forEach { hg ->
row {
hg.headers.forEach { h ->
cell { TableCellText(flexRender(h.column.columnDef.header, h.getContext()), bold = true) }
}
}
}
table.getRowModel().rows.forEach { r ->
row {
r.getVisibleCells().forEach { c ->
cell { TableCellText(flexRender(c.column.columnDef.cell, c.getContext())) }
}
}
}
}The engine is design-free — what you render is up to you. TableGrid is the
adapter's drop-in SubcomposeLayout for the common case (content-sized
columns, optional column-span cells, IME-safe nested text fields); skip it and
draw cells yourself if you need a different layout.
TanStack Table is the de-facto headless table engine on the web. There was no equivalent for Kotlin Multiplatform / Compose Multiplatform: existing Compose table libraries bake their own opinion into the engine and rendering, leaving you to either accept their choices or rebuild from scratch. This project ports the engine 1:1 from TanStack's TypeScript source, layers a Compose Multiplatform adapter on top, and ships them as separate Gradle modules so you take only what you need.
| Module | Targets |
|---|---|
:table-core |
commonMain (JVM 11, Android, iosX64, iosArm64, iosSimulatorArm64) |
:table-compose |
commonMain (JVM, Android, all three iOS targets) |
Compose Multiplatform 1.7.x. Kotlin 2.x. Android minSdk 24, compileSdk 35.
0.1.0 is built with Kotlin 2.1.0 and Compose Multiplatform 1.7.3.
.klibs are tied to the
Kotlin compiler version. For the iOS targets, keep your project's Kotlin
version aligned with 2.1.x — a significantly different Kotlin version can
fail to link against the native artifacts. This is a general Kotlin
Multiplatform constraint, not specific to this library.:table-core — the engine. No UI dependencies. Mirrors the TanStack
Table v8 API: Table, ColumnDef, createColumnHelper, getCoreRowModel,
getSortedRowModel, getFilteredRowModel, getGroupedRowModel,
getExpandedRowModel, getPaginationRowModel, getFacetedRowModel, and
the column / row / cell feature mixins (visibility, ordering, pinning,
sizing, filtering, sorting, grouping, expansion, pagination, selection).:table-compose — the Compose Multiplatform adapter. Provides
rememberTable (subscribes the composition to the engine's state),
flexRender (resolves cell / header / footer templates), and
TableGrid (a theme-neutral SubcomposeLayout grid).:sample — a Compose Multiplatform demo app covering 13 feature
screens. Runs on Android and iOS from one common source set.Available on Maven Central. Add mavenCentral() to your repositories, then the
dependencies to your shared module:
// settings.gradle.kts
dependencyResolutionManagement {
repositories {
mavenCentral()
}
}
// build.gradle.kts (your KMP module)
kotlin {
sourceSets {
commonMain.dependencies {
// The Compose Multiplatform adapter. Pulls in table-core transitively
// (it api-depends on the engine), so this one line is enough for most apps.
implementation("io.github.praveenshharma:table-compose:0.1.0")
// Or depend on just the headless engine — no Compose, no UI:
// implementation("io.github.praveenshharma:table-core:0.1.0")
}
}
}The API mirrors TanStack Table v8 — the TanStack Table docs
remain the canonical reference for engine concepts (row models, column
definitions, state). The differences live in Kotlin's syntax and a handful of
type-system adaptations documented in docs/INTERNALS.md.
Kotlin API reference (generated by Dokka, regenerated on every push to main):
https://praveenshharma.github.io/tanstack-table-kmp/.
A complete walkthrough for each feature lives in the :sample app — open the
example screen for the feature you want and follow the column definitions
through the rememberTable call into the TableGrid builder.
Cell composables that take a single stable parameter (a Row, Column,
TableColumn, or Header) are skipped by Compose's stability-inference
restart logic when the surrounding table state changes but the parameter
reference does not. Annotate your cell composables @NonRestartableComposable
to opt out of the skip and re-run them on every parent recomposition. The
adapter's own cell composables (TableGrid and friends) already do this;
your own cell composables need the annotation if they read engine state.
@Composable
@NonRestartableComposable
fun StatusCell(row: Row<Person>) {
val isSelected = row.getIsSelected()
// ...
}Background on why this is necessary lives in
docs/INTERNALS.md under "Compose adapter".
Each of the 13 example screens in :sample is self-contained: open the file,
follow the column definitions, the rememberTable call, and the TableGrid
builder. The sample app runs on Android (./gradlew :sample:installDebug)
and iOS (see docs/INTERNALS.md for the simulator
setup).
| Screen | Demonstrates |
|---|---|
| Basic | Core column rendering, footers |
| Sorting | Single + multi-column sort, custom sort fns |
| Filters | Per-column text filtering |
| Filters (Faceted) | Faceted unique-value / min-max filtering |
| Pagination | Page size, page navigation, manual vs auto |
| Grouping | Group rows by column, aggregation, leaf-row counts |
| Expanding | Sub-row expansion, row-level expand state |
| Row Selection | Single / multi-row selection, indeterminate header state |
| Row Pinning | Pin rows to the top or bottom |
| Column Pinning | Pin columns to left / right |
| Column Ordering | Reorder columns at runtime |
| Column Visibility | Show / hide individual columns |
| Column Groups | Nested header groups (multi-row headers) |
table-core/ — headless engine (Kotlin Multiplatform)
table-compose/ — Compose Multiplatform adapter
sample/ — demo app (Android + iOS)
iosApp/ — minimal Swift host for the iOS simulator demo
docs/INTERNALS.md — design notes and implementation invariants
See CONTRIBUTING.md. PRs are welcome; bug reports and
reproduction projects in particular. For substantial changes please open an
issue first to discuss direction.
MIT. This project is a community-maintained Kotlin Multiplatform
port of TanStack Table (also MIT,
© Tanner Linsley); see NOTICE for attribution. It is not
affiliated with, endorsed by, or sponsored by TanStack.