Flab UIv0.0.2
MobileA2UI

Data table

A dense, sortable, selectable table built on TanStack Table v9. Columns are authored as TanStack column definitions (use createDataTableColumnHelper for value inference). Sorting, selection, global search and column filters all run through the table's row model, and the composites Flab is built from — avatars, status pills, inline meters — live inside cells.

Sortable and selectable

Ada Okaforada@northwind.io
active
82%
Bruno Satobruno@northwind.io
invited
12%
Carmen Diazcarmen@northwind.io
suspended
47%
Dae-jung Parkdae@northwind.io
active
68%
Emeka Obiemeka@northwind.io
invited
5%
const column = createDataTableColumnHelper<Row>();
const columns: DataTableColumn<Row>[] = [  column.accessor((r) => r.name, {    id: 'name', header: 'Member',    cell: ({ row }) => <CellAvatar name={row.original.name} secondary={row.original.email} />,  }),  column.accessor((r) => r.status, {    id: 'status', header: 'Status',    cell: ({ row }) => <CellBadge tone={statusTone[row.original.status]} label={row.original.status} dot />,  }),  column.accessor((r) => r.usage, {    id: 'usage', header: 'Usage', meta: { align: 'right', width: '12rem' },    cell: ({ row }) => <CellMeter value={row.original.usage} label={`${row.original.usage}%`} />,  }),];
<DataTable data={rows} columns={columns} selectable getRowId={(r) => r.email} />

Multi-column sorting

Every column sorts on click, cycling ascending → descending → off. Hold Shift while clicking a second header to sort by more than one column; the click order sets the priority.

Ada Okaforada@northwind.io
active
82%
Bruno Satobruno@northwind.io
invited
12%
Carmen Diazcarmen@northwind.io
suspended
47%
Dae-jung Parkdae@northwind.io
active
68%
Emeka Obiemeka@northwind.io
invited
5%
// Shift-click a second header to append it as a secondary sort.<DataTable data={rows} columns={columns} getRowId={(r) => r.email} />

Search, filters and toolbar

A global search box, per-column filter controls, and a right-aligned actions slot. Each column declares its control through meta.filter: text, select, multi-select, or a number range. The multi-select control comes with a search box and a select-all toggle, so long option lists stay manageable.

Ada Okaforada@northwind.io
active
82%
Bruno Satobruno@northwind.io
invited
12%
Carmen Diazcarmen@northwind.io
suspended
47%
Dae-jung Parkdae@northwind.io
active
68%
Emeka Obiemeka@northwind.io
invited
5%
const filterColumns: DataTableColumn<Row>[] = [  column.accessor((r) => r.name, {    id: 'name', header: 'Member',    meta: { filter: { variant: 'text', label: 'Filter members' } },    cell: ({ row }) => <CellAvatar name={row.original.name} secondary={row.original.email} />,  }),  column.accessor((r) => r.status, {    id: 'status', header: 'Status',    meta: { filter: { variant: 'multi-select', label: 'Status', placeholder: 'All statuses', options: [      { label: 'Active', value: 'active' },      { label: 'Invited', value: 'invited' },      { label: 'Suspended', value: 'suspended' },    ] } },    cell: ({ row }) => <CellBadge tone={statusTone[row.original.status]} label={row.original.status} dot />,  }),  column.accessor((r) => r.usage, {    id: 'usage', header: 'Usage',    meta: { align: 'right', filter: { variant: 'number-range', label: 'Usage' } },    cell: ({ row }) => <CellMeter value={row.original.usage} label={`${row.original.usage}%`} />,  }),];
<DataTable  data={rows}  columns={filterColumns}  getRowId={(r) => r.email}  searchable  toolbarActions={<Button variant="secondary" size="sm"><Download className="size-3.5" /> Export</Button>}/>

Column header filters

The same meta.filter controls can live on the column headers instead of the toolbar, AG-Grid style. Set filterPlacement="header" and each filterable column gets a funnel button that opens its control in a popover; the funnel fills in when that column is actively filtered.

Ada Okaforada@northwind.io
active
82%
Bruno Satobruno@northwind.io
invited
12%
Carmen Diazcarmen@northwind.io
suspended
47%
Dae-jung Parkdae@northwind.io
active
68%
Emeka Obiemeka@northwind.io
invited
5%
// Same filterColumns as above (each declares meta.filter).<DataTable  data={rows}  columns={filterColumns}  getRowId={(r) => r.email}  filterPlacement="header"/>

Client pagination

Pass paginated to page a local array. The pager below the table has a page-size select and first / previous / next / last controls; filtering resets to the first page.

Member 01member1@northwind.io
active
0%
Member 02member2@northwind.io
invited
37%
Member 03member3@northwind.io
suspended
74%
Member 04member4@northwind.io
active
11%
Member 05member5@northwind.io
invited
48%
Rows per page
Page 1 of 5
<DataTable data={manyRows} columns={columns} getRowId={(r) => r.email} paginated pageSize={5} />

Server-side (manual) mode

For server-driven data, set manualPagination / manualSorting and hand the table only the current page plus a rowCount. The table renders what it is given and reports sort and page changes so you can refetch.

Members (server-side)
Member 01member1@northwind.io
active
0%
Member 02member2@northwind.io
invited
37%
Member 03member3@northwind.io
suspended
74%
Member 04member4@northwind.io
active
11%
Member 05member5@northwind.io
invited
48%
Rows per page
Page 1 of 5
const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 5 });const [sorting, setSorting] = useState([]);// Fetch the page from your API whenever pagination/sorting change.const { pageRows, total } = useServerData({ pagination, sorting });
<DataTable  data={pageRows}  columns={columns}  getRowId={(r) => r.email}  paginated  manualPagination  manualSorting  rowCount={total}  pagination={pagination}  onPaginationChange={setPagination}  sorting={sorting}  onSortingChange={setSorting}/>

Virtualized (large data)

Pass virtualized with a maxHeight to window the rows: the table scrolls smoothly over very large datasets while only the visible slice stays in the DOM. This example renders 10,000 rows. Virtualization is an alternative to paginated — set one or the other, not both. Sorting, selection, filters and pinned columns keep working.

// 10,000 rows, but only the visible window is rendered.<DataTable  data={hugeRows}  columns={columns}  getRowId={(r) => r.email}  selectable  virtualized  maxHeight={420}/>

Column visibility

Pass viewOptions for a “Columns” control that toggles which columns show. Set enableHiding: false on a column to keep it always visible. Visibility is controllable via columnVisibility.

Ada Okaforada@northwind.io
active
82%
Bruno Satobruno@northwind.io
invited
12%
Carmen Diazcarmen@northwind.io
suspended
47%
Dae-jung Parkdae@northwind.io
active
68%
Emeka Obiemeka@northwind.io
invited
5%
<DataTable data={rows} columns={columns} getRowId={(r) => r.email} viewOptions />

Column layout: pin, resize, reorder

Pin a column with meta.pinned so it stays in view while scrolling sideways; turn on resizableColumns for drag handles and reorderableColumns to drag headers into a new order. A minWidth keeps the table wide enough to scroll.

Drag a column border to resize it, or double-click the border to snap the column to the width of its widest visible cell. The same best-fit sizing is available imperatively through apiRef as autosizeColumn(id) and autosizeColumns().

Ada Okaforada@northwind.io
activeada@northwind.io
82%
Bruno Satobruno@northwind.io
invitedbruno@northwind.io
12%
Carmen Diazcarmen@northwind.io
suspendedcarmen@northwind.io
47%
Dae-jung Parkdae@northwind.io
activedae@northwind.io
68%
Emeka Obiemeka@northwind.io
invitedemeka@northwind.io
5%
const columns: DataTableColumn<Row>[] = [  column.accessor((r) => r.name, {    id: 'name', header: 'Member', meta: { pinned: 'left' },    cell: ({ row }) => <CellAvatar name={row.original.name} secondary={row.original.email} />,  }),  // …status, usage];
<DataTable data={rows} columns={columns} getRowId={(r) => r.email}  resizableColumns reorderableColumns minWidth="48rem" />

Grouped headers

Nest columns under a group to render a spanning header row.

Member
Ada Okaforactive
82%
Bruno Satoinvited
12%
Carmen Diazsuspended
47%
Dae-jung Parkactive
68%
Emeka Obiinvited
5%
const columns = [  column.group({    id: 'member', header: 'Member',    columns: column.columns([      column.accessor((r) => r.name, { id: 'name', header: 'Name' }),      column.accessor((r) => r.status, { id: 'status', header: 'Status' }),    ]),  }),  column.accessor((r) => r.usage, { id: 'usage', header: 'Usage' }),];

Row grouping and subtotals

Set groupBy to a column id to group rows under collapsible headers. A column with meta.aggregate (here 'sum') shows a per-group subtotal, formatted through its valueFormatter. Groups start expanded; click a group to collapse it. Use renderGroupHeader for a full-width custom summary row.

$20,000
Ada Okafor$12,000
Bruno Sato$8,000
$21,000
Carmen Diaz$15,000
Dae-jung Park$6,000
$3,000
Emeka Obi$3,000
const groupingColumns: DataTableColumn<Deal>[] = [  dealColumn.accessor((d) => d.stage, { id: 'stage', header: 'Stage' }),  dealColumn.accessor((d) => d.owner, { id: 'owner', header: 'Owner' }),  dealColumn.accessor((d) => d.value, {    id: 'value',    header: 'Value',    meta: { align: 'right', aggregate: 'sum', valueFormatter: (v) => `$${Number(v).toLocaleString()}` },  }),];
<DataTable data={deals} columns={groupingColumns} groupBy="stage" getRowId={(d) => d.id} />

Empty and loading

No members yet.
<DataTable data={[]} columns={columns} isLoading /><DataTable data={[]} columns={columns} empty="No members yet." />

Error

Couldn't load members. Retry.
<DataTable data={[]} columns={columns} error="Couldn't load members. Retry." />

Props

PropTypeDefaultNotes
dataTData[]—Row objects.
columnsDataTableColumn<TData>[]—TanStack column definitions.
selectablebooleanfalseAdds a selection checkbox column.
rowSelection / onRowSelectionChangeRowSelectionState—Controlled selection keyed by row id; uncontrolled by default.
getRowId(row, i) => string—Stable row id; defaults to the index.
sorting / onSortingChangeSortingState—Controlled sort; uncontrolled by default. onSortingChange fires either way.
enableMultiSortbooleantrueAllow Shift-click to sort by multiple columns.
searchable / searchPlaceholderboolean / string—Shows a global search box in the toolbar.
globalFilter / onGlobalFilterChangestring—Controlled global search text.
columnFilters / onColumnFiltersChangeColumnFiltersState—Controlled column filters. Declare controls via column meta.filter.
filterPlacement'toolbar' | 'header''toolbar'Render meta.filter controls in the toolbar, or as funnel popovers on column headers.
viewOptionsbooleanfalseAdds a "Columns" popover to toggle column visibility.
columnVisibility / onColumnVisibilityChangeColumnVisibilityState—Controlled column visibility; uncontrolled by default.
toolbar / toolbarActions / renderToolbarboolean / ReactNode / (table) => ReactNode—Toggle, add actions to, or fully replace the toolbar (DataTableToolbar).
resizableColumns / reorderableColumnsboolean—Drag to resize (double-click a border to best-fit) / drag headers to reorder.
columnPinning / columnSizing / columnOrdercontrolled state—Controlled layout; initial pinning also via column meta.pinned.
apiRef{ current: DataTableApi }—Imperative Grid/Column API: pin, reorder, resize, column state, grouping.
groupBy / grouping / onGroupingChangestring | string[] / GroupingState—Groups rows by column id(s) into collapsible sections.
expanded / defaultExpanded / onExpandedChangeExpandedState—Controls which groups are open. Defaults to all expanded.
renderGroupHeader(ctx) => ReactNode—Full-width custom group header; ctx has leafRows, count, toggle, colSpan.
meta.valueFormatter(value) => ReactNode—Formats a cell for display when no cell renderer is given.
meta.aggregate'sum' | 'avg' | 'min' | 'max' | 'count' | 'extent'—Per-group subtotal for the column when grouped.
paginated / pageSize / pageSizeOptionsboolean / number / number[]—Enables client pagination and renders a pager.
pagination / onPaginationChangePaginationState—Controlled pagination; uncontrolled by default.
manualPagination / manualSorting / manualFilteringboolean—Server-side mode: the caller supplies each processed page.
rowCount / pageCountnumber—Total rows/pages for manual pagination. pageCount -1 if unknown.
virtualizedbooleanfalseWindows rows for large data (TanStack Virtual). Needs maxHeight; excludes paginated.
maxHeightnumber | string—Viewport height for the scroll container. Required when virtualized.
estimatedRowHeight / overscannumber36 / 8Virtualizer row-height estimate and rows rendered beyond the viewport.
isLoadingboolean—Shows shimmer rows.
error / emptyReactNode—Error and empty states.