Data Table
A fully-featured, generic data table built on TanStack Table v8. Supports server/client pagination, sorting, global search, column visibility with localStorage persistence, row selection, bulk actions, trashed/archived record filter toggle, and skeleton loading all opt-in.
Interactive Demo
Three variants shown: full-featured with selection and bulk actions, flat with no pagination, and a simple filtered list.
Full-Featured Table
Row selection · Sorting · Search · Column visibility · Page size · Bulk actions · Trashed toggle
| # | Status | ||||||
|---|---|---|---|---|---|---|---|
| 1 | EMP-001 | Nehry Dedoro | ICT | Lead Architect | active | ||
| 2 | EMP-002 | Reyna Garcia | Finance | Budget Officer | active | ||
| 3 | EMP-003 | Mark Alvarez | HR | HRMO | on leave | ||
| 4 | EMP-004 | Clara Bautista | Engineering | Civil Engineer | active | ||
| 5 | EMP-005 | Jose Reyes | ICT | Systems Analyst | inactive |
Page 1 of 3
Flat Table (No Pagination)
Simple, compact display for small datasets. No toolbar, no footer.
| # | Status | ||
|---|---|---|---|
| 1 | Nehry Dedoro | ICT | active |
| 2 | Reyna Garcia | Finance | active |
| 3 | Mark Alvarez | HR | on leave |
| 4 | Clara Bautista | Engineering | active |
| 5 | Jose Reyes | ICT | inactive |
Simple Filtered Table
Search-only toolbar, client pagination, no selection.
| # | Status | ||
|---|---|---|---|
| 1 | Nehry Dedoro | ICT | active |
| 2 | Reyna Garcia | Finance | active |
| 3 | Mark Alvarez | HR | on leave |
| 4 | Clara Bautista | Engineering | active |
Page 1 of 3
Features & Props Matrix
Quick reference mapping of DataTable capabilities to their enabling properties and helpers.
| Feature | Enabling Props / Helpers | Default | Description |
|---|---|---|---|
| Client-Side Pagination | pagination="client" | "client" | Automatic in-memory pagination handled by TanStack Table. |
| Server-Side Pagination | pagination="server" currentPage, totalPages, totalCount onPageChange, onPageSizeChange | - | Delegates pagination calculations to a database/API. Required for large datasets. |
| Client-Side Sorting | enableSorting={true} | true | Interactive sorting by clicking column headers. Handled automatically in-memory. |
| Server-Side Sorting | manualSorting={true} onSortingChange | false | Bypasses client-side sorting and triggers a callback when sorting state changes. |
| Global Search | enableSearch={true} onSearchChange (optional) | true | Global filter input. If `onSearchChange` is provided, debounces input by 300ms for API queries. |
| Row Selection & Bulk Actions | enableRowSelection={true} selectionColumn<T>() toolbarProps.bulkActions | false | Allows multi-row checking. Shows a contextual bulk actions tray (inline or floating). |
| Column Visibility Persistence | enableColumnVisibility={true} tableId="unique_key" | false | Renders a columns dropdown toggle. Persists toggled state to `localStorage` keyed by `tableId`. |
| Soft-Delete (Trashed) Records | enableTrashed={true} trashed, onTrashedChange | false | Toggles viewing archived or deleted items, displaying safety warning banners. |
| Skeleton Loading States | isLoading={true} loadingRowCount={5} | false | Renders skeleton animation placeholders instead of rows to reflect initial load state. |
Installation
Install the core DataTable component. The useTableQuery hook is a separate optional install.
Core component
pnpm dlx shadcn@latest add https://micto-ui-kit.misangono.net/r/micto/data-table.jsonBasic Usage
Define columns with createColumnHelper, add built-in factories, and pass everything to DataTable.
import {
DataTable,
createColumnHelper,
selectionColumn,
rowActionsColumn,
} from "@/components/micto/data-table"
import { ToolbarAction } from "@/components/micto/table-toolbar"
import { Button } from "@/components/ui/button"
import { Eye, Pencil, Trash2, Plus } from "lucide-react"
type Employee = { id: string; name: string; department: string }
const col = createColumnHelper<Employee>()
const columns = [
selectionColumn<Employee>(),
col.accessor("name", { header: "Name" }),
col.accessor("department", { header: "Department" }),
rowActionsColumn<Employee>({
actions: () => [
{ label: "View", icon: Eye, onClick: (r) => console.log(r) },
{ label: "Edit", icon: Pencil, onClick: (r) => console.log(r) },
{ label: "Delete", icon: Trash2, variant: "destructive", onClick: (r) => console.log(r) },
],
}),
]
const employees: Employee[] = [
{ id: "1", name: "Juan Dela Cruz", department: "ICT" },
{ id: "2", name: "Maria Clara", department: "HR" },
]
export default function EmployeesPage() {
return (
<DataTable
data={employees}
columns={columns}
tableId="employees"
enableRowSelection
enableColumnVisibility
pagination="client"
pageSize={10}
pageSizeOptions={[10, 25, 50]}
toolbarProps={{
actions: (
<Button>
<Plus className="mr-2 h-4 w-4" /> Add Employee
</Button>
),
bulkActions: (rows) => (
<ToolbarAction icon={Trash2} variant="destructive">
Delete {rows.length}
</ToolbarAction>
),
}}
/>
)
}Flat Table (No Pagination)
For small datasets like deduction lists, DTR daily logs, or readonly summaries just the table, nothing else.
import { DataTable } from "@/components/micto/data-table"
type Deduction = { name: string; amount: number }
const deductions: Deduction[] = [
{ name: "SSS", amount: 500 },
{ name: "PhilHealth", amount: 250 },
]
const deductionColumns = [
{ accessorKey: "name", header: "Deduction Name" },
{ accessorKey: "amount", header: "Amount" },
]
export default function FlatTable() {
return (
<DataTable
data={deductions}
columns={deductionColumns}
pagination={false}
toolbar={false}
density="compact"
/>
)
}Server-Side Mode
Own the page, sort, and search state yourself. DataTable renders the UI, you provide the data fetching logic.
import * as React from "react"
import { DataTable, ColumnDef, SortingState } from "@/components/micto/data-table"
type User = { id: string; name: string; email: string }
const columns: ColumnDef<User>[] = [
{ accessorKey: "name", header: "Name" },
{ accessorKey: "email", header: "Email" },
]
export default function ServerPaginatedTable() {
const [data, setData] = React.useState<User[]>([])
const [isLoading, setIsLoading] = React.useState(false)
const [page, setPage] = React.useState(1)
const [pageSize, setPageSize] = React.useState(10)
const [totalPages, setTotalPages] = React.useState(1)
const [totalCount, setTotalCount] = React.useState(0)
const [sorting, setSorting] = React.useState<SortingState>([])
const [search, setSearch] = React.useState("")
// Trigger data fetching in your component using page, pageSize, sorting, search...
return (
<DataTable
data={data}
columns={columns}
isLoading={isLoading}
pagination="server"
currentPage={page}
totalPages={totalPages}
totalCount={totalCount}
onPageChange={setPage}
pageSize={pageSize}
pageSizeOptions={[10, 25, 50]}
onPageSizeChange={setPageSize}
manualSorting
onSortingChange={setSorting}
onSearchChange={setSearch} // debounced 300ms, fires your API
/>
)
}Laravel Inertia.js Mode
Seamlessly bind Laravel paginators to DataTable. All search, sort, and trashed filter events update the URL via Inertia router.get() with preserveState.
import React from "react"
import { router } from "@inertiajs/react"
import {
DataTable,
createColumnHelper,
indexColumn,
rowActionsColumn,
} from "@/components/micto/data-table"
import { Button } from "@/components/ui/button"
import { Plus, Eye, Pencil, Trash2 } from "lucide-react"
type Employee = {
id: string;
name: string;
department: string;
deleted_at: string | null;
}
const col = createColumnHelper<Employee>()
const columns = [
indexColumn<Employee>(),
col.accessor("name", { header: "Name" }),
col.accessor("department", { header: "Department" }),
rowActionsColumn<Employee>({
actions: (row) => [
{ label: "View Profile", icon: Eye, onClick: (r) => router.get(`/employees/${r.id}`) },
{ label: "Edit", icon: Pencil, onClick: (r) => router.get(`/employees/${r.id}/edit`) },
{ label: "Delete", icon: Trash2, variant: "destructive", onClick: (r) => router.delete(`/employees/${r.id}`) },
],
}),
]
export default function EmployeesIndex({ records, filters }: {
records: { data: Employee[]; current_page: number; last_page: number; total: number };
filters: { search?: string; sorting?: unknown; trashed?: string; pageSize?: number };
}) {
// Helper to update Inertia URL params while preserving page state/scroll
const updateQuery = (newParams: Record<string, unknown>) => {
router.get(
window.location.pathname,
{ ...filters, ...newParams },
{ preserveState: true, preserveScroll: true }
)
}
return (
<DataTable
data={records.data}
columns={columns}
pagination="server"
currentPage={records.current_page}
totalPages={records.last_page}
totalCount={records.total}
pageSize={Number(filters.pageSize ?? 10)}
pageSizeOptions={[10, 25, 50]}
// Filter Handlers -> Triggers Inertia router.get()
onPageChange={(page) => updateQuery({ page })}
onPageSizeChange={(pageSize) => updateQuery({ pageSize, page: 1 })}
onSearchChange={(search) => updateQuery({ search, page: 1 })}
onSortingChange={(sorting) => updateQuery({ sorting, page: 1 })}
// Trashed / Soft Delete Toggle
enableTrashed={true}
trashed={filters.trashed === "true"}
onTrashedChange={(trashed) => updateQuery({ trashed, page: 1 })}
toolbarProps={{
actions: (
<Button size="sm" className="h-8 text-xs" onClick={() => router.get("/employees/create")}>
<Plus className="size-3.5 mr-1.5" />
Add Employee
</Button>
),
}}
/>
)
}Trashed / Archived Toggle
Toggle soft-deleted records in and out of the active view. Shows an indicator pill in the toolbar and a dismissible warning banner when viewing archived data.
import * as React from "react"
import { DataTable, ColumnDef } from "@/components/micto/data-table"
type Employee = { id: string; name: string }
const columns: ColumnDef<Employee>[] = [
{ accessorKey: "name", header: "Name" },
]
const activeEmployees: Employee[] = [{ id: "1", name: "Juan Dela Cruz" }]
const trashedEmployees: Employee[] = [{ id: "2", name: "Maria Clara (Archived)" }]
export default function TrashedExample() {
const [isTrashed, setIsTrashed] = React.useState(false)
// In client mode, swap data. In server mode, pass trashed parameter to API query
const currentData = isTrashed ? trashedEmployees : activeEmployees
return (
<DataTable
data={currentData}
columns={columns}
enableTrashed
trashed={isTrashed}
onTrashedChange={setIsTrashed}
trashedLabel="Show Trashed"
trashedActiveLabel="Viewing Trashed"
/>
)
}Column Factories
Three pre-built column factories ship with the component so you never write checkbox, row number, or actions columns from scratch again.
__select__Helper: selectionColumn<T>()
- Fixed width of 40px
- Displays a header checkbox to select/deselect all rows on the page
- Stops row click event propagation automatically to prevent accidental row selection
- Disabled from sorting or visibility hiding behaviors
__index__Helper: indexColumn<T>()
- Fixed width of 50px
- Renders a 1-indexed row number (#) that matches the current layout row order
- Uses
tabular-numsCSS styling to ensure clean vertical alignment - Disabled from sorting or visibility hiding behaviors
__actions__Helper: rowActionsColumn<T>()
- Fixed width of 50px
- Renders a vertical ellipsis menu containing custom icons, labels, callbacks, and separators
- Supports conditional disabling, destructive variants, and item visibility filtering
- Disabled from sorting or visibility hiding behaviors
import {
selectionColumn, // checkbox with select-all
indexColumn, // row number (#)
rowActionsColumn, // ⋮ actions dropdown
createColumnHelper,
} from "@/components/micto/data-table"
import { Eye, Pencil, UserX, Trash2 } from "lucide-react"
type Employee = {
id: string
name: string
status: "active" | "inactive"
}
// Conceptual router & helper overrides
const router = {
push: (path: string) => console.log("Navigate to", path),
}
const openEditModal = (employee: Employee) => console.log("Edit", employee)
const deactivate = (employee: Employee) => console.log("Deactivate", employee)
const deleteEmployee = (employee: Employee) => console.log("Delete", employee)
const col = createColumnHelper<Employee>()
const columns = [
selectionColumn<Employee>(),
indexColumn<Employee>({ header: "#" }),
col.accessor("name", {
header: "Full Name",
cell: (info) => (
<span className="font-medium">{info.row.original.name}</span>
),
}),
rowActionsColumn<Employee>({
// actions receives the full row data object; use it to conditionally hide/disable
actions: (row) => [
{ label: "View", icon: Eye, onClick: (r) => router.push(`/employees/${r.id}`) },
{ label: "Edit", icon: Pencil, onClick: (r) => openEditModal(r) },
{ label: "Deactivate", icon: UserX, onClick: (r) => deactivate(r), separator: true },
{
label: "Delete",
icon: Trash2,
variant: "destructive",
disabled: row.status === "inactive",
onClick: (r) => deleteEmployee(r),
},
],
}),
]Core Props
Required and identity configuration.
| Prop | Type | Default | Description |
|---|---|---|---|
| data | TData[] | Array of row objects. Generic works with any shape. | |
| columns | ColumnDef<TData, any>[] | TanStack Table column definitions. Use createColumnHelper or the built-in factories. | |
| tableId | string | undefined | Unique key for this table instance. When set, column visibility state is persisted to localStorage. |
| isLoading | boolean | false | Renders skeleton rows matching the column layout instead of data rows. |
| loadingRowCount | number | 5 | Number of skeleton placeholder rows shown during loading. |
| emptyState | ReactNode | built-in | Custom zero-state content. Defaults to a search icon with 'No results found' message. |
Toolbar Props
Configure the built-in TableToolbar or replace it entirely.
| Prop | Type | Default | Description |
|---|---|---|---|
| enableToolbar | boolean | true | Set to false to completely hide the toolbar component. |
| toolbar | ReactNode | false | undefined | Full custom toolbar override. Pass false to hide the toolbar entirely. |
| toolbarProps.filters | ReactNode | undefined | Extra filter controls placed left of the search input inside the built-in toolbar. |
| toolbarProps.activeFiltersCount | number | undefined | Count of currently active filters, used to show a reset button or indicator. |
| toolbarProps.actions | ReactNode | undefined | Right-side action buttons (e.g. Add button) shown when no rows are selected. |
| toolbarProps.bulkActions | ReactNode | (rows: TData[]) => ReactNode | undefined | Actions shown in the bulk tray when rows are selected. Receives selected rows when passed as a function. |
| toolbarProps.toolbarVariant | 'inline' | 'floating' | 'inline' | Controls how the toolbar renders bulk actions morphing inline or as a floating dock. |
Search Props
Single internal search input client-side automatic, server-side debounced callback.
| Prop | Type | Default | Description |
|---|---|---|---|
| enableSearch | boolean | true | Shows the built-in global search input inside the toolbar. |
| searchPlaceholder | string | 'Search...' | Placeholder text for the search input. |
| initialSearch | string | '' | Initial value for the search input/filter. |
| onSearchChange | (value: string) => void | undefined | Debounced (300ms) callback for server-side search. Client-side filtering is automatic when omitted. |
Pagination Props
Three modes: off, client-managed, or server-controlled.
| Prop | Type | Default | Description |
|---|---|---|---|
| pagination | false | 'client' | 'server' | 'client' | false = no pagination. client = TanStack manages pages. server = you control state. |
| currentPage | number | undefined | Active page number (1-indexed). Required in server mode. |
| totalPages | number | undefined | Total number of pages. Required in server mode. |
| totalCount | number | undefined | Total row count shown in the footer label. |
| onPageChange | (page: number) => void | undefined | Called when the user navigates to a different page. |
| pageSize | number | 10 | Initial number of rows per page. |
| pageSizeOptions | number[] | undefined | If provided, renders a 'Rows per page' selector in the footer. Example: [10, 25, 50]. |
| onPageSizeChange | (size: number) => void | undefined | Called when the user changes the rows-per-page selection. |
Interaction Props
Sorting, column visibility, row selection, and click handlers.
| Prop | Type | Default | Description |
|---|---|---|---|
| enableSorting | boolean | true | Enables sortable column headers with sort direction icons. |
| manualSorting | boolean | false | Set true for server-side sorting. Disables client-side sort logic. |
| initialSorting | SortingState | [] | Initial sorting configuration. |
| onSortingChange | (sorting: SortingState) => void | undefined | Called when sort state changes. Use with manualSorting to trigger server requests. |
| enableColumnVisibility | boolean | false | Adds a 'Columns' toggle button to the toolbar for showing/hiding columns. |
| initialColumnVisibility | VisibilityState | {} | Initial column visibility configuration. |
| enableRowSelection | boolean | (row) => boolean | false | Enables checkbox column. Pass a function to conditionally enable per row. |
| onRowSelectionChange | (rows: TData[]) => void | undefined | Called whenever the row selection set changes. Receives full row objects. |
| onRowClick | (row: TData, event) => void | undefined | Called when a row is clicked. Adds cursor-pointer and hover highlight. Ignored on checkbox/action cells. |
| onCellClick | (value, columnId, row, event) => void | undefined | Called when a specific cell is clicked. Useful for clipboard copy or inline drill-down. |
Trashed Filter Props
Optional toggle for soft-deleted / archived records.
| Prop | Type | Default | Description |
|---|---|---|---|
| enableTrashed | boolean | false | Adds a 'Show Trashed' toggle button to the toolbar for viewing archived/soft-deleted records. |
| trashed | boolean | undefined | Controlled trashed state. When omitted, internal state is managed automatically. |
| onTrashedChange | (trashed: boolean) => void | undefined | Called when the user toggles the trashed filter. Server mode re-fetches data. |
| trashedLabel | string | 'Show Trashed' | Label for the inactive toggle button. |
| trashedActiveLabel | string | 'Viewing Trashed' | Label for the active tinted badge toggle. |
Presentation Props
Row density, sticky header, and styling overrides.
| Prop | Type | Default | Description |
|---|---|---|---|
| density | 'compact' | 'default' | 'comfortable' | 'default' | Controls row height and font size. compact=xs padding, comfortable=relaxed padding. |
| stickyHeader | boolean | false | Makes the table header sticky on scroll. Best for long tables inside a bounded container. |
| className | string | undefined | Additional CSS classes for the outer wrapper div. |
| tableClassName | string | undefined | Additional CSS classes for the table container border/card. |
| tableRef | React.RefObject<Table<TData> | null> | undefined | Ref forwarded to the raw TanStack Table instance for power user access. |