Command Palette

Search for a command to run...

GitHub
ComponentsData Table

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.

React
TanStack Table v8
Generic

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

0
selected
#Status
1EMP-001Nehry DedoroICTLead Architect
active
2EMP-002Reyna GarciaFinanceBudget Officer
active
3EMP-003Mark AlvarezHRHRMO
on leave
4EMP-004Clara BautistaEngineeringCivil Engineer
active
5EMP-005Jose ReyesICTSystems Analyst
inactive

Page 1 of 3

Rows per page

Flat Table (No Pagination)

Simple, compact display for small datasets. No toolbar, no footer.

#Status
1Nehry DedoroICT
active
2Reyna GarciaFinance
active
3Mark AlvarezHR
on leave
4Clara BautistaEngineering
active
5Jose ReyesICT
inactive

Simple Filtered Table

Search-only toolbar, client pagination, no selection.

0
selected
#Status
1Nehry DedoroICT
active
2Reyna GarciaFinance
active
3Mark AlvarezHR
on leave
4Clara BautistaEngineering
active

Page 1 of 3

Features & Props Matrix

Quick reference mapping of DataTable capabilities to their enabling properties and helpers.

FeatureEnabling Props / HelpersDefaultDescription
Client-Side Paginationpagination="client""client"Automatic in-memory pagination handled by TanStack Table.
Server-Side Paginationpagination="server"
currentPage, totalPages, totalCount
onPageChange, onPageSizeChange
-Delegates pagination calculations to a database/API. Required for large datasets.
Client-Side SortingenableSorting={true}trueInteractive sorting by clicking column headers. Handled automatically in-memory.
Server-Side SortingmanualSorting={true}
onSortingChange
falseBypasses client-side sorting and triggers a callback when sorting state changes.
Global SearchenableSearch={true}
onSearchChange (optional)
trueGlobal filter input. If `onSearchChange` is provided, debounces input by 300ms for API queries.
Row Selection & Bulk ActionsenableRowSelection={true}
selectionColumn<T>()
toolbarProps.bulkActions
falseAllows multi-row checking. Shows a contextual bulk actions tray (inline or floating).
Column Visibility PersistenceenableColumnVisibility={true}
tableId="unique_key"
falseRenders a columns dropdown toggle. Persists toggled state to `localStorage` keyed by `tableId`.
Soft-Delete (Trashed) RecordsenableTrashed={true}
trashed, onTrashedChange
falseToggles viewing archived or deleted items, displaying safety warning banners.
Skeleton Loading StatesisLoading={true}
loadingRowCount={5}
falseRenders 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.json

Basic 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.

Selection Column
__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 Column
__index__

Helper: indexColumn<T>()

  • Fixed width of 50px
  • Renders a 1-indexed row number (#) that matches the current layout row order
  • Uses tabular-nums CSS styling to ensure clean vertical alignment
  • Disabled from sorting or visibility hiding behaviors
Row Actions Column
__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.

PropTypeDefaultDescription
dataTData[]Array of row objects. Generic works with any shape.
columnsColumnDef<TData, any>[]TanStack Table column definitions. Use createColumnHelper or the built-in factories.
tableIdstringundefinedUnique key for this table instance. When set, column visibility state is persisted to localStorage.
isLoadingbooleanfalseRenders skeleton rows matching the column layout instead of data rows.
loadingRowCountnumber5Number of skeleton placeholder rows shown during loading.
emptyStateReactNodebuilt-inCustom zero-state content. Defaults to a search icon with 'No results found' message.

Toolbar Props

Configure the built-in TableToolbar or replace it entirely.

PropTypeDefaultDescription
enableToolbarbooleantrueSet to false to completely hide the toolbar component.
toolbarReactNode | falseundefinedFull custom toolbar override. Pass false to hide the toolbar entirely.
toolbarProps.filtersReactNodeundefinedExtra filter controls placed left of the search input inside the built-in toolbar.
toolbarProps.activeFiltersCountnumberundefinedCount of currently active filters, used to show a reset button or indicator.
toolbarProps.actionsReactNodeundefinedRight-side action buttons (e.g. Add button) shown when no rows are selected.
toolbarProps.bulkActionsReactNode | (rows: TData[]) => ReactNodeundefinedActions 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.

PropTypeDefaultDescription
enableSearchbooleantrueShows the built-in global search input inside the toolbar.
searchPlaceholderstring'Search...'Placeholder text for the search input.
initialSearchstring''Initial value for the search input/filter.
onSearchChange(value: string) => voidundefinedDebounced (300ms) callback for server-side search. Client-side filtering is automatic when omitted.

Pagination Props

Three modes: off, client-managed, or server-controlled.

PropTypeDefaultDescription
paginationfalse | 'client' | 'server''client'false = no pagination. client = TanStack manages pages. server = you control state.
currentPagenumberundefinedActive page number (1-indexed). Required in server mode.
totalPagesnumberundefinedTotal number of pages. Required in server mode.
totalCountnumberundefinedTotal row count shown in the footer label.
onPageChange(page: number) => voidundefinedCalled when the user navigates to a different page.
pageSizenumber10Initial number of rows per page.
pageSizeOptionsnumber[]undefinedIf provided, renders a 'Rows per page' selector in the footer. Example: [10, 25, 50].
onPageSizeChange(size: number) => voidundefinedCalled when the user changes the rows-per-page selection.

Interaction Props

Sorting, column visibility, row selection, and click handlers.

PropTypeDefaultDescription
enableSortingbooleantrueEnables sortable column headers with sort direction icons.
manualSortingbooleanfalseSet true for server-side sorting. Disables client-side sort logic.
initialSortingSortingState[]Initial sorting configuration.
onSortingChange(sorting: SortingState) => voidundefinedCalled when sort state changes. Use with manualSorting to trigger server requests.
enableColumnVisibilitybooleanfalseAdds a 'Columns' toggle button to the toolbar for showing/hiding columns.
initialColumnVisibilityVisibilityState{}Initial column visibility configuration.
enableRowSelectionboolean | (row) => booleanfalseEnables checkbox column. Pass a function to conditionally enable per row.
onRowSelectionChange(rows: TData[]) => voidundefinedCalled whenever the row selection set changes. Receives full row objects.
onRowClick(row: TData, event) => voidundefinedCalled when a row is clicked. Adds cursor-pointer and hover highlight. Ignored on checkbox/action cells.
onCellClick(value, columnId, row, event) => voidundefinedCalled when a specific cell is clicked. Useful for clipboard copy or inline drill-down.

Trashed Filter Props

Optional toggle for soft-deleted / archived records.

PropTypeDefaultDescription
enableTrashedbooleanfalseAdds a 'Show Trashed' toggle button to the toolbar for viewing archived/soft-deleted records.
trashedbooleanundefinedControlled trashed state. When omitted, internal state is managed automatically.
onTrashedChange(trashed: boolean) => voidundefinedCalled when the user toggles the trashed filter. Server mode re-fetches data.
trashedLabelstring'Show Trashed'Label for the inactive toggle button.
trashedActiveLabelstring'Viewing Trashed'Label for the active tinted badge toggle.

Presentation Props

Row density, sticky header, and styling overrides.

PropTypeDefaultDescription
density'compact' | 'default' | 'comfortable''default'Controls row height and font size. compact=xs padding, comfortable=relaxed padding.
stickyHeaderbooleanfalseMakes the table header sticky on scroll. Best for long tables inside a bounded container.
classNamestringundefinedAdditional CSS classes for the outer wrapper div.
tableClassNamestringundefinedAdditional CSS classes for the table container border/card.
tableRefReact.RefObject<Table<TData> | null>undefinedRef forwarded to the raw TanStack Table instance for power user access.