DataTable
Composed table experience with toolbar, card view, filters, pagination, and selection.
Quick Preview
Track applicant status, budget, source, and ownership.
Buyer | Customer ID | Email | Status | Budget | Address | Location | Source | Assigned To | Next Action | Date Added | ||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
Ada Okaforada.okafor@giddaa.com | CUS-4823 | ada.okafor@giddaa.com | Approved | ₦85m | No 5A Beta Glass Road, Off Awolowo Way, Ikoyi, Lagos | Ikoyi, Lagos | Website | Maya | Send offer | May 19, 2026 | ||
Nnamdi Kalunnamdi.kalu@giddaa.com | CUS-4824 | nnamdi.kalu@giddaa.com | Review | ₦64m | Wisdom Estate, Behind BG Musa Station, Central Business District, Abuja | Wuse, Abuja | Referral | Tomi | Schedule tour | May 20, 2026 | ||
Zainab Bellozainab.bello@giddaa.com | CUS-4825 | zainab.bello@giddaa.com | Approved | ₦120m | Plot 14 Aso Drive, Asokoro District, Abuja, Federal Capital Territory | Asokoro, Abuja | Agent | Ife | Verify docs | May 21, 2026 | ||
Emeka Ezeemeka.eze@giddaa.com | CUS-4826 | emeka.eze@giddaa.com | Review | ₦42m | No 23 Admiralty Way, Lekki Phase 1, Lagos | Lekki, Lagos | Maya | Follow up | May 22, 2026 | |||
Fatima Musafatima.musa@giddaa.com | CUS-4827 | fatima.musa@giddaa.com | Approved | ₦98m | House 7, Diplomatic Zone, Maitama, Abuja | Maitama, Abuja | Website | Tomi | Send invoice | May 23, 2026 |
Showing 1 to 5 of 10 entries
Usage
import {
DataTable,
DataTableBulkActions,
DataTableCardGrid,
DataTableFilter,
DataTablePageSizeSelect,
DataTablePagination,
DataTableRoot,
DataTableToolbar,
useDataTable,
type DataTableColumnDef,
} from "@giddaa-housing/ui/data-table";
import { Button } from "@giddaa-housing/ui/button";
import { TableCard } from "@giddaa-housing/ui/table-card";
import { getPaginationRowModel } from "@tanstack/react-table";
import { DataTableViewSelect } from "@/components/recipes/data-table-view-select";
function ApplicantsTable({ columns, data }) {
const table = useDataTable({
columns,
data,
enableRowSelection: true,
getPaginationRowModel: getPaginationRowModel(),
});
return (
<DataTableRoot table={table}>
<DataTableToolbar>
<DataTableFilter
fields={filterFields}
onApply={(serialized) => {
setFilter(serialized);
table.setPageIndex(0);
}}
/>
<DataTableViewSelect view={view} onViewChange={setView} />
<DataTablePageSizeSelect />
</DataTableToolbar>
<DataTable />
<DataTableBulkActions>
{({ data }) => <Button>Assign {data.length}</Button>}
</DataTableBulkActions>
<DataTablePagination />
</DataTableRoot>
);
}Examples
Default Data Table
Use the primitives to create a product-specific wrapper. The wrapper owns filters, search, routing, export buttons, and API state while the UI package keeps the table layout consistent. This example places Filter inside a toolbar popover and applies the selected status outside the table primitive.
Track applicant status, budget, source, and ownership.
Buyer | Customer ID | Email | Status | Budget | Address | Location | Source | Assigned To | Next Action | Date Added | ||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
Ada Okaforada.okafor@giddaa.com | CUS-4823 | ada.okafor@giddaa.com | Approved | ₦85m | No 5A Beta Glass Road, Off Awolowo Way, Ikoyi, Lagos | Ikoyi, Lagos | Website | Maya | Send offer | May 19, 2026 | ||
Nnamdi Kalunnamdi.kalu@giddaa.com | CUS-4824 | nnamdi.kalu@giddaa.com | Review | ₦64m | Wisdom Estate, Behind BG Musa Station, Central Business District, Abuja | Wuse, Abuja | Referral | Tomi | Schedule tour | May 20, 2026 | ||
Zainab Bellozainab.bello@giddaa.com | CUS-4825 | zainab.bello@giddaa.com | Approved | ₦120m | Plot 14 Aso Drive, Asokoro District, Abuja, Federal Capital Territory | Asokoro, Abuja | Agent | Ife | Verify docs | May 21, 2026 | ||
Emeka Ezeemeka.eze@giddaa.com | CUS-4826 | emeka.eze@giddaa.com | Review | ₦42m | No 23 Admiralty Way, Lekki Phase 1, Lagos | Lekki, Lagos | Maya | Follow up | May 22, 2026 | |||
Fatima Musafatima.musa@giddaa.com | CUS-4827 | fatima.musa@giddaa.com | Approved | ₦98m | House 7, Diplomatic Zone, Maitama, Abuja | Maitama, Abuja | Website | Tomi | Send invoice | May 23, 2026 |
Showing 1 to 5 of 10 entries
This example also references copy-and-own recipes:
- DataTableViewSelect for the toolbar list/card view switcher.
- ActionsMenu for row and card actions.
Toolbar Filter
Use DataTableFilter for the common toolbar popover. It composes Filter,
Popover, FilterContent, FilterResetButton, and FilterApplyButton; your
screen still owns what applying filters does.
<DataTableToolbar>
<DataTableFilter
fields={filterFields}
onApply={(serialized, filters) => {
setFilter(serialized);
setStructuredFilters(filters);
table.setPageIndex(0);
}}
/>
<DataTablePageSizeSelect />
</DataTableToolbar>Props
| Prop/API | Type | Default | Description |
|---|---|---|---|
useDataTable | UseDataTableOptions | - | Convenience hook around useReactTable with Giddaa defaults and optional row-selection column. |
DataTableRoot | { table, isLoading?, onRowClick? } | - | Provider/root that shares the TanStack table instance with child components. |
DataTableToolbar | div props | - | Layout shell for search, filters, page-size select, export buttons, or app actions. |
DataTableFilter | Filter props plus popover/trigger/button overrides | - | Convenience toolbar popover for filter rows. Calls onApply; does not mutate table rows, fetch data, or write URL params for you. |
DataTable | { table?, isLoading?, emptyState? } | - | Styled table renderer using the shared TanStack instance. |
DataTableCardGrid | { renderCard?, titleColumnId?, detailColumnIds? } | - | Card-grid renderer using the shared TanStack instance. Use renderCard for product-specific card layouts. |
DataTableBulkActions | render prop or node | - | Sticky bulk-action bar shown when rows are selected. |
DataTablePagination | { table?, page?, pageSize?, pageCount?, totalEntries?, onPageChange?, showControls?, controlAppearance?, showJumpToPage? } | - | Pagination footer with entry summary, page controls, and jump-to-page. Reads TanStack pagination state by default, or drive it from API state. |
DataTablePageSizeSelect | { table?, pageSizeOptions?, pageSize?, onPageSizeChange?, label? } | - | Page-size select. Reads TanStack state by default, or drive it from API state with pageSize + onPageSizeChange. |
Pagination
Pagination and page size can be driven two ways. Both use the same DataTablePagination and DataTablePageSizeSelect components — pick per screen based on where the source of truth lives.
Client-side (TanStack owns pagination)
When the table has all the rows, add getPaginationRowModel and TanStack slices, counts, and navigates for you. The controls bind to the table instance automatically (via prop or DataTableRoot context):
const table = useDataTable({
columns,
data, // full dataset
getPaginationRowModel: getPaginationRowModel(),
});
<DataTablePageSizeSelect /> // reads/writes table.setPageSize
<DataTablePagination /> // reads/writes table.setPageIndexServer-side (API owns pagination)
Real screens usually fetch one page at a time and get page, pageSize, and a total count back from the API. There are two supported approaches:
A — Controlled TanStack (manualPagination). Keep using the table instance but let your API be the source of truth. TanStack won't slice data; it just tracks state and reports getPageCount() from rowCount:
const table = useDataTable({
columns,
data: pageRows, // only the current page from the API
manualPagination: true,
rowCount: totalCount, // total across all pages, from the API
state: { pagination: { pageIndex: page - 1, pageSize } },
onPaginationChange: (updater) => {
const next =
typeof updater === "function"
? updater({ pageIndex: page - 1, pageSize })
: updater;
setPage(next.pageIndex + 1);
setPageSize(next.pageSize);
},
});
<DataTablePageSizeSelect />
<DataTablePagination />B — Fully controlled (no table wiring). If pagination lives entirely in your API/query state and you don't want to thread it through TanStack, pass the values directly. In this mode neither control touches the table instance:
<DataTablePageSizeSelect
pageSize={pageSize}
onPageSizeChange={(size) => {
setPageSize(size);
setPage(1); // reset to first page on size change
}}
/>
<DataTablePagination
page={page} // 1-based current page
pageSize={pageSize}
totalEntries={totalCount}
onPageChange={setPage} // called with the next 1-based page
/>Notes:
pageis 1-based; TanStack's internalpageIndexis 0-based. The controlled props andonPageChangeall speak 1-based pages so they map directly to typical API params.- Pass
totalEntries+pageSizeto render the "Showing 11 to 20 of 132 entries" summary and derive page count. If your API already returns a page count, passpageCounttoo. - Use
showControls,showPreviousNext,showJumpToPage, andshowSummaryto hide footer sections per screen. - Use
controlAppearance="icon"for compact previous/next buttons orcontrolAppearance="control"for labelled Previous/Next buttons. DataTablePaginationrenders nothing whenpageCount <= 1, in either mode.- Reset to page 1 when the page size, search, or filters change so you don't request an out-of-range page.
Views
Keep view switching in your application wrapper. The root provides the TanStack table once, then each view can render the same row model differently.
Use the DataTableViewSelect recipe when you want the standard toolbar select for list/card switching.
<DataTableRoot table={table}>
<DataTableToolbar>{/* view toggle, filters, search */}</DataTableToolbar>
{view === "list" ? <DataTable /> : null}
{view === "card" ? (
<DataTableCardGrid
renderCard={(row, { action, selection }) => (
<TableCard
action={action}
selection={selection}
title={row.original.name}
subtitle={row.original.email}
details={[
{ label: "Budget", value: row.original.budget },
{ label: "Location", value: row.original.location },
]}
/>
)}
/>
) : null}
{view === "kanban" ? <ApplicantKanban table={table} /> : null}
<DataTablePagination />
</DataTableRoot>For simple card views, you can map columns directly:
<DataTableCardGrid
titleColumnId="name"
subtitleColumnId="email"
badgeColumnId="status"
detailColumnIds={["budget", "location", "assignedTo"]}
/>For richer screens, prefer renderCard. This avoids forcing your table columns to match the card layout, especially when title and subtitle are composed inside the same table cell. Kanban should stay app-owned until the interaction model stabilizes; it can consume useDataTableContext() or the same table instance.
Accessibility
- Provide accessible column headers and row action labels.
- Keep router search params, API fetching, and product filters in the application wrapper.
- Keep row click behavior separate from interactive controls inside cells.