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
View Code
View Code
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
Search
- 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
searchTermandonSearchTermChange. AddmanualSearchwhen 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
columnFiltersandonColumnFiltersChange. AddmanualFilteringwhen the server does the filtering
Sorting
- Column sorting with ascending/descending order
- Default sort column and direction
- Controlled mode - Use
sortColumn,sortDirection, andonSortChange. AddmanualSortingwhen 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, andtotalItems - Page size options (10, 20, 50, 100 by default)
- Results summary showing current range and total
- Can be disabled for small datasets
Props
| Prop | Type | Default | Required | Description |
|---|---|---|---|---|
data | T[] | - | Yes | Array of data objects to display |
columns | TableColumn<T>[] | - | Yes | Column definitions (see TableColumn interface) |
searchable | boolean | true | No | Enable global search functionality |
searchPlaceholder | string | "Search..." | No | Placeholder text for search input |
searchKeys | Array<keyof T | string> | undefined | No | Specific columns to search (defaults to all columns) |
onSearch | (searchTerm: string, filteredData: T[]) => void | undefined | No | Callback when search term changes |
searchTerm | string | undefined | No | Controlled search term (for server-side search) |
onSearchTermChange | (searchTerm: string) => void | undefined | No | Callback when search term changes (controlled mode) |
filterable | boolean | false | No | Enable column filtering |
filters | ColumnFilter<T>[] | [] | No | Filter definitions (see ColumnFilter interface) |
onFilter | (filters: Record<string, unknown>, filteredData: T[]) => void | undefined | No | Callback when filters change |
columnFilters | Record<string, unknown> | undefined | No | Controlled column filters (for server-side filtering) |
onColumnFiltersChange | (filters: Record<string, unknown>) => void | undefined | No | Callback when filters change (controlled mode) |
defaultSortColumn | string | undefined | No | Default column to sort by |
defaultSortDirection | "asc" | "desc" | "asc" | No | Default sort direction |
sortColumn | string | undefined | No | Controlled sort column (for server-side sorting) |
sortDirection | "asc" | "desc" | undefined | No | Controlled sort direction (for server-side sorting) |
onSortChange | (column: string, direction: "asc" | "desc") => void | undefined | No | Callback when sort changes (controlled mode) |
defaultPageSize | number | 10 | No | Default number of items per page |
currentPage | number | undefined | No | Controlled current page (for server-side pagination) |
pageSize | number | undefined | No | Controlled page size (for server-side pagination) |
onPageChange | (page: number) => void | undefined | No | Callback when page changes (controlled mode) |
onPageSizeChange | (size: number) => void | undefined | No | Callback when page size changes (controlled mode) |
totalItems | number | undefined | No | Total number of items (required for server-side pagination) |
pageSizeOptions | number[] | [10, 20, 50, 100] | No | Available page size options |
showPagination | boolean | true | No | Show pagination controls |
showSearch | boolean | true | No | Show search bar |
showFilters | boolean | true | No | Show filter controls |
rowKey | keyof T | ((record: T) => string | number) | "id" | No | Key to identify rows |
emptyText | string | "No data found" | No | Text to show when no data |
className | string | undefined | No | Additional CSS classes |
variant | "default" | "striped" | "bordered" | "hover" | "elevated" | "interactive" | "default" | No | Table visual variant |
size | "sm" | "md" | "lg" | "md" | No | Table size |
loading | boolean | false | No | Show loading state |
caption | string | undefined | No | Table caption (for accessibility) |
onRowClick | (record: T, index: number) => void | undefined | No | Callback when row is clicked |
selection | SelectionConfig<T> | undefined | No | Row 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
Basic DataTable with Search
<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
rowKeyfor 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
Related Components
- 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.