Skip to Content
ComponentsDataTable

DataTable

The DataTable component is an advanced table with built-in search, filtering, sorting, and pagination. It’s built on top of the Table component and provides enhanced data management capabilities.

Live Examples

Live Example
☀️
Copy
View Code
import { DataTable, type TableColumn } from '@jarllyng/nostromo' const data = [ { id: 1, name: 'John Doe', email: 'john@example.com', age: 28, department: 'Engineering', status: 'Active' }, { id: 2, name: 'Jane Smith', email: 'jane@example.com', age: 32, department: 'Marketing', status: 'Active' }, { id: 3, name: 'Bob Johnson', email: 'bob@example.com', age: 45, department: 'Sales', status: 'Inactive' }, { id: 4, name: 'Alice Brown', email: 'alice@example.com', age: 29, department: 'Engineering', status: 'Active' } ] const columns: TableColumn<(typeof data)[number]>[] = [...
Live Example
☀️
Copy
View Code
import { DataTable, type TableColumn } from '@jarllyng/nostromo' const data = [ { id: 1, name: 'John Doe', email: 'john@example.com', department: 'Engineering', status: 'Active' }, { id: 2, name: 'Jane Smith', email: 'jane@example.com', department: 'Marketing', status: 'Active' }, { id: 3, name: 'Bob Johnson', email: 'bob@example.com', department: 'Sales', status: 'Inactive' }, { id: 4, name: 'Alice Brown', email: 'alice@example.com', department: 'Engineering', status: 'Active' } ] const columns: TableColumn<(typeof data)[number]>[] = [...

Import

import { DataTable } from "@jarllyng/nostromo"; // or import { DataTable } from "@jarllyng/nostromo/data-table";

Usage

import { DataTable, type TableColumn } from "@jarllyng/nostromo"; const data = [ { id: 1, name: "John Doe", email: "john@example.com", department: "Engineering", }, { id: 2, name: "Jane Smith", email: "jane@example.com", department: "Marketing", }, ]; const columns: TableColumn<(typeof data)[number]>[] = [ { key: "name", title: "Name", dataIndex: "name", sortable: true }, { key: "email", title: "Email", dataIndex: "email" }, { key: "department", title: "Department", dataIndex: "department", sortable: true, }, ]; export default function Example() { return ( <DataTable data={data} columns={columns} searchable={true} showPagination={true} /> ); }

Features

  • Global search across multiple columns
  • Custom search keys to specify which columns to search
  • Real-time filtering as you type
  • Search callback for custom handling
  • Controlled mode - Use searchTerm and onSearchTermChange. Add manualSearch when the server does the searching

Filtering

  • Column filters with multiple filter types (text, select, number, date, boolean)
  • Multiple filters can be applied simultaneously
  • Custom filter functions for advanced filtering logic
  • Clear filters button to reset all filters
  • Controlled mode - Use columnFilters and onColumnFiltersChange. Add manualFiltering when the server does the filtering

Sorting

  • Column sorting with ascending/descending order
  • Default sort column and direction
  • Controlled mode - Use sortColumn, sortDirection, and onSortChange. Add manualSorting when the server does the sorting
  • Sort indicators in column headers

Pagination

  • Client-side pagination - Automatic pagination of data (default)
  • Server-side pagination - Controlled mode with currentPage, pageSize, onPageChange, onPageSizeChange, and totalItems
  • Page size options (10, 20, 50, 100 by default)
  • Results summary showing current range and total
  • Can be disabled for small datasets

Props

PropTypeDefaultRequiredDescription
dataT[]-YesArray of data objects to display
columnsTableColumn<T>[]-YesColumn definitions (see TableColumn interface)
searchablebooleantrueNoEnable global search functionality
searchPlaceholderstring"Search..."NoPlaceholder text for search input
searchKeysArray<keyof T | string>undefinedNoSpecific columns to search (defaults to all columns)
onSearch(searchTerm: string, filteredData: T[]) => voidundefinedNoCallback when search term changes
searchTermstringundefinedNoControlled search term (for server-side search)
onSearchTermChange(searchTerm: string) => voidundefinedNoCallback when search term changes (controlled mode)
filterablebooleanfalseNoEnable column filtering
filtersColumnFilter<T>[][]NoFilter definitions (see ColumnFilter interface)
onFilter(filters: Record<string, unknown>, filteredData: T[]) => voidundefinedNoCallback when filters change
columnFiltersRecord<string, unknown>undefinedNoControlled column filters (for server-side filtering)
onColumnFiltersChange(filters: Record<string, unknown>) => voidundefinedNoCallback when filters change (controlled mode)
defaultSortColumnstringundefinedNoDefault column to sort by
defaultSortDirection"asc" | "desc""asc"NoDefault sort direction
sortColumnstringundefinedNoControlled sort column (for server-side sorting)
sortDirection"asc" | "desc"undefinedNoControlled sort direction (for server-side sorting)
onSortChange(column: string, direction: "asc" | "desc") => voidundefinedNoCallback when sort changes (controlled mode)
defaultPageSizenumber10NoDefault number of items per page
currentPagenumberundefinedNoControlled current page (for server-side pagination)
pageSizenumberundefinedNoControlled page size (for server-side pagination)
onPageChange(page: number) => voidundefinedNoCallback when page changes (controlled mode)
onPageSizeChange(size: number) => voidundefinedNoCallback when page size changes (controlled mode)
totalItemsnumberundefinedNoTotal number of items (required for server-side pagination)
pageSizeOptionsnumber[][10, 20, 50, 100]NoAvailable page size options
showPaginationbooleantrueNoShow pagination controls
showSearchbooleantrueNoShow search bar
showFiltersbooleantrueNoShow filter controls
rowKeykeyof T | ((record: T) => string | number)"id"NoKey to identify rows
emptyTextstring"No data found"NoText to show when no data
classNamestringundefinedNoAdditional CSS classes
variant"default" | "striped" | "bordered" | "hover" | "elevated" | "interactive""default"NoTable visual variant
size"sm" | "md" | "lg""md"NoTable size
loadingbooleanfalseNoShow loading state
captionstringundefinedNoTable caption (for accessibility)
onRowClick(record: T, index: number) => voidundefinedNoCallback when row is clicked
selectionSelectionConfig<T>undefinedNoRow selection configuration (see Table component)

ColumnFilter Interface

interface ColumnFilter<T = Record<string, unknown>> { key: string; // Column key to filter type: "text" | "select" | "number" | "date" | "boolean"; options?: Array<{ // For 'select' type label: string; value: string | number; }>; placeholder?: string; // Placeholder for filter input filterFn?: (value: unknown, record: T) => boolean; // Custom filter function }

Examples

<DataTable data={employees} columns={columns} searchable={true} searchPlaceholder="Search employees..." />

DataTable with Filters

<DataTable data={employees} columns={columns} searchable={true} filterable={true} filters={[ { key: "department", type: "select", options: [ { label: "Engineering", value: "Engineering" }, { label: "Marketing", value: "Marketing" }, ], }, { key: "status", type: "select", options: [ { label: "Active", value: "Active" }, { label: "Inactive", value: "Inactive" }, ], }, ]} />

DataTable with Custom Search Keys

<DataTable data={employees} columns={columns} searchable={true} searchKeys={["name", "email"]} // Only search in name and email columns searchPlaceholder="Search by name or email..." />

DataTable with Default Sort

<DataTable data={employees} columns={columns} defaultSortColumn="name" defaultSortDirection="asc" />

DataTable without Pagination

<DataTable data={employees} columns={columns} searchable={true} showPagination={false} // Show all results />

DataTable with Server-Side Pagination (Controlled Mode)

import { useEffect, useState } from "react"; import { DataTable, type TableColumn } from "@jarllyng/nostromo"; type Employee = { id: number; name: string; email: string }; const columns: TableColumn<Employee>[] = [ { key: "name", title: "Name", dataIndex: "name", sortable: true }, { key: "email", title: "Email", dataIndex: "email" }, ]; export default function ServerSideDataTable() { const [rows, setRows] = useState<Employee[]>([]); const [currentPage, setCurrentPage] = useState(1); const [pageSize, setPageSize] = useState(10); const [sortColumn, setSortColumn] = useState("name"); const [sortDirection, setSortDirection] = useState<"asc" | "desc">("asc"); const [searchTerm, setSearchTerm] = useState(""); const [totalItems, setTotalItems] = useState(0); // Every controlled prop the table reads is also a dependency here: the server // is doing the paging, sorting and searching, so a change to any of them is a // new request rather than a re-filter of what is already on screen. useEffect(() => { let cancelled = false; fetch( `/api/employees?page=${currentPage}&pageSize=${pageSize}&sort=${sortColumn}&direction=${sortDirection}&search=${searchTerm}`, ) .then((response) => response.json()) .then((body: { data: Employee[]; total: number }) => { if (cancelled) return; setRows(body.data); setTotalItems(body.total); }); return () => { cancelled = true; }; }, [currentPage, pageSize, sortColumn, sortDirection, searchTerm]); return ( <DataTable data={rows} columns={columns} // Controlled props for server-side operations currentPage={currentPage} pageSize={pageSize} totalItems={totalItems} onPageChange={setCurrentPage} onPageSizeChange={setPageSize} sortColumn={sortColumn} sortDirection={sortDirection} onSortChange={(col, dir) => { setSortColumn(col); setSortDirection(dir); }} searchTerm={searchTerm} onSearchTermChange={setSearchTerm} /> ); }

Accessibility

  • ✅ WCAG 2.1 AA compliant
  • ✅ Keyboard navigation for all interactive elements
  • ✅ ARIA labels for search and filter inputs
  • ✅ Screen reader support with proper table structure
  • ✅ Focus management for pagination controls

Do’s and Don’ts

✅ Do

  • Use DataTable for large datasets that need search/filter
  • Provide meaningful column titles
  • Use appropriate filter types for each column
  • Set reasonable default page sizes
  • Include rowKey for proper row identification

❌ Don’t

  • Use DataTable for simple static tables (use Table instead)
  • Overload with too many filters
  • Use very large page sizes (>100)
  • Forget to handle empty states
  • Use without pagination for large datasets
  • Table - Basic table component
  • Input - Used for search input
  • Select - Used for filter dropdowns
  • Pagination - Used for pagination controls

Controlled is not the same as server-side

Passing searchTerm, columnFilters or sortColumn makes DataTable controlled: it stops owning that piece of state and tells you when the user changes it. It does not stop it processing the rows you give it.

That distinction matters as soon as a server is involved. A server searching without accent distinctions returns { name: "José" } for the query jose, and a local substring match then drops the row while totalItems still counts it. Controlled sorting has the same shape: the page arrives in the server’s order and gets reordered locally.

Say who does the work:

<DataTable data={rows} // already searched, filtered and sorted by the server columns={columns} manualSearch manualFiltering manualSorting searchTerm={query} onSearchTermChange={setQuery} columnFilters={filters} onColumnFiltersChange={setFilters} sortColumn={sort.column} sortDirection={sort.direction} onSortChange={(column, direction) => setSort({ column, direction })} currentPage={page} onPageChange={setPage} totalItems={total} />

In manual mode the rows and their order are kept exactly as given, and the callbacks still fire so you know what to ask the server for next.

Pagination has always worked this way, under a different name: it is manual when currentPage and totalItems are both given.

onSortChange fires once

One click, one call, carrying both the new column and the new direction. Writing both values back from it is safe:

onSortChange={(column, direction) => setSort({ column, direction })}

It used to emit twice, the second call carrying the previous column, so a consumer doing exactly the above ended up back where it started and sent the server two requests.

Filter values keep their type

A boolean filter is a three-state select: all, yes, no. It was a text box, and Boolean("false") is true, so asking for the false records returned the true ones.

A number filter that is emptied applies no filter. Number("") is 0, so clearing the box used to leave only the rows whose value was zero. An explicitly entered 0 still filters for zero.

Both controls show 0 and false rather than blanking them, which is what String(value || "") did to legitimate values.

Last updated on