Data Table
Low-level table primitives plus a typed DataTable with sorting, row selection, and pagination.
Overview
This page documents two layers that ship from the same Table.kt source:
- Table primitives —
Table,TableHeader,TableBody,TableFooter,TableRow,TableHead,TableCell,TableCaption. Use these when you want full control over layout, custom headers, or non-rectangular data. DataTable<T>— a typed, batteries-included data table. It owns state for sorting, optional checkbox selection, and Previous/Next pagination, and lets you swap in a custom pagination UI through a slot.
Both layers share the same horizontal-scroll model: the table is not shrunk to viewport on mobile. Declare each column's width as a fixed Dp and the wrapper will scroll horizontally when needed. Header and body cells in the same column must share the same Modifier.width(...) so the columns line up.
Anatomy
Table {
TableHeader {
TableRow {
TableHead { /* header cell */ }
// ...one TableHead per column
}
}
TableBody {
TableRow {
TableCell { /* body cell */ }
// ...one TableCell per column
}
// The final body row should pass showBottomBorder = false
}
TableFooter { // optional
TableRow(showBottomBorder = false) {
TableCell { /* footer cell */ }
}
}
}
TableCaption(text = "Optional caption") // rendered outside the Table containerTable primitives
A hand-rolled invoice table. Each column declares a fixed Dp width and both TableHead and TableCell cells reuse the same width so the columns stay aligned under the horizontal scroller. The last body row passes showBottomBorder = false to drop its bottom border.
With footer and caption
TableFooter paints a muted background and a top border — typically used for totals. The footer row's showBottomBorder = false keeps the rounded corner clean. TableCaption is rendered outside the Table container so it sits below the scrollable area.
Data Table
DataTable<T> accepts your full unsorted, unpaginated items list and a list of DataTableColumn<T> definitions. It handles sorting, optional selection, and Previous/Next pagination internally — pass onSortChange / onSelectionChange if you need to observe.
Each column declares:
id— stable identifier used as the sort key and for visibility.header— header label (also used as the accessible label whenheaderContentis provided).width— fixedDpwidth applied to both header and body cells in this column.cell— renders a single body cell from one item.
Sorting
Set sortable = true on a column and supply a comparator. Clicking the header cycles through NONE → ASC → DESC → NONE. The active column shows an up/down arrow; unsorted sortable columns show a UnfoldMore icon. Observe changes with onSortChange, and seed an initial state with initialSort = SortState(columnId = "amount", direction = SortDirection.DESC).
Row selection
Set enableSelection = true and DataTable auto-prepends a checkbox column (selectionColumnWidth = 48.dp by default). The header checkbox toggles all rows on the current page, and onSelectionChange emits the full Set<T> of selected items across all pages. A summary line below the table reads "N of M row(s) selected.".
The underlying
Checkboxdoesn't support an indeterminate visual, so the header checkbox is "checked" iff every row on the current page is selected. Toggling it adds or removes the current page's rows from the selection set.
Custom pagination
The pagination slot defaults to DefaultPagination — Previous / Next outline buttons aligned to the end. To replace it, pass your own @Composable PaginationScope.() -> Unit. PaginationScope exposes page, pageCount, canPrev, canNext, prev(), next(), and goTo(page) so you can render numbered pages, jump-to controls, or anything else.
API reference
Table
| Parameter | Type | Default | Description |
|---|---|---|---|
modifier | Modifier | Modifier | Modifier applied to the outer container. |
scrollState | ScrollState | rememberScrollState() | Horizontal scroll state shared by header and body. |
content | @Composable ColumnScope.() -> Unit | — | Table sections — typically TableHeader, TableBody, and TableFooter. |
TableRow
| Parameter | Type | Default | Description |
|---|---|---|---|
modifier | Modifier | Modifier | Pass Modifier.width(...) to make rows span the same total width under Table's horizontal scroll. |
onClick | (() -> Unit)? | null | Optional click handler. When non-null the row becomes clickable. |
selected | Boolean | false | When true, the row paints a muted background. |
showBottomBorder | Boolean | true | When false, the row omits its bottom border — pass false for the final body row. |
content | @Composable RowScope.() -> Unit | — | Cells of the row — typically TableHead or TableCell. |
TableHead / TableCell
Both accept a modifier: Modifier and a content: @Composable RowScope.() -> Unit. Pass Modifier.width(...) to set the column's width — header and body cells in the same column should share the same width.
TableHead: 40.dp min-height, 8.dp horizontal padding, medium-weight muted text.TableCell: 8.dp all-around padding, foreground text color.
TableCaption
| Parameter | Type | Default | Description |
|---|---|---|---|
text | String | — | Caption text. |
modifier | Modifier | Modifier | Modifier applied to the caption. |
Render TableCaption outside the Table container.
DataTable<T>
| Parameter | Type | Default | Description |
|---|---|---|---|
items | List<T> | — | Full unsorted, unpaginated data set. |
columns | List<DataTableColumn<T>> | — | Column definitions. |
rowKey | (T) -> Any | — | Stable key for an item. Used for selection identity. |
modifier | Modifier | Modifier | Modifier applied to the outer container. |
enableSelection | Boolean | false | When true a leading checkbox column is auto-prepended. |
selectionColumnWidth | Dp | 48.dp | Width of the auto-injected selection column. |
pageSize | Int | 10 | Rows per page. |
initialSort | SortState | SortState() | Initial sort state. |
onSortChange | ((SortState) -> Unit)? | null | Observer fired when sort changes. |
onSelectionChange | ((Set<T>) -> Unit)? | null | Observer fired when selection changes. |
onRowClick | ((T) -> Unit)? | null | Optional row click handler. |
caption | String? | null | Optional caption text rendered below the table. |
pagination | @Composable PaginationScope.() -> Unit | DefaultPagination | Slot rendered as a full-width row below the summary line. |
The summary row above the pagination slot reads "N of M row(s) selected." (when enableSelection = true) on the left and "Page X of Y" on the right.
DataTableColumn<T>
| Field | Type | Description |
|---|---|---|
id | String | Stable identifier used as the sort key and visibility key. |
header | String | Header label text. Also the accessible label when headerContent is provided. |
width | Dp | Fixed width applied to header and body cells in this column. |
sortable | Boolean = false | When true, clicking the header cycles NONE → ASC → DESC → NONE. Requires comparator. |
comparator | Comparator<T>? | Comparator used when sortable is true. |
headerContent | (@Composable RowScope.() -> Unit)? | Optional custom header cell content. When null, header is rendered as a medium-weight muted label. |
cell | @Composable RowScope.(T) -> Unit | Renders one cell for the given row item. |
SortState / SortDirection
enum class SortDirection { NONE, ASC, DESC }
data class SortState(
val columnId: String? = null,
val direction: SortDirection = SortDirection.NONE,
)PaginationScope
The receiver for the pagination slot. Use it to render any pagination UI you like.
| Member | Type | Description |
|---|---|---|
page | Int | Current page index (0-based). |
pageCount | Int | Total number of pages. Always at least 1. |
canPrev | Boolean | true when prev() will advance backward. |
canNext | Boolean | true when next() will advance forward. |
prev() | Unit | Go to the previous page if canPrev. |
next() | Unit | Go to the next page if canNext. |
goTo(page) | Unit | Jump to an absolute 0-based page index (coerced in range). |
The default slot (DefaultPagination) renders Previous / Next outline buttons aligned to the end of a full-width row.