The DataTable component is a powerful enterprise feature built on TanStack Table that provides sorting, filtering, pagination, column visibility, grouping, and virtualization out of the box.
Basic usage
import { DataTable, DataTableColumnDef } from '@raystack/apsara';
interface User {
id: string;
name: string;
email: string;
status: 'active' | 'inactive';
}
const columns: DataTableColumnDef<User, unknown>[] = [
{
accessorKey: 'name',
header: 'Name',
enableSorting: true
},
{
accessorKey: 'email',
header: 'Email',
enableSorting: true
},
{
accessorKey: 'status',
header: 'Status',
enableColumnFilter: true,
filterType: 'select'
}
];
function UserTable() {
const data: User[] = [
{ id: '1', name: 'John Doe', email: '[email protected]', status: 'active' },
{ id: '2', name: 'Jane Smith', email: '[email protected]', status: 'inactive' }
];
return (
<DataTable
data={data}
columns={columns}
defaultSort={{ name: 'name', order: 'asc' }}
>
<DataTable.Toolbar>
<DataTable.Search placeholder="Search users..." />
<DataTable.Filters />
<DataTable.DisplayControls />
</DataTable.Toolbar>
<DataTable.Content />
</DataTable>
);
}
Server-side mode
Use server-side mode for large datasets with backend pagination and filtering.
import { DataTable, DataTableQuery } from '@raystack/apsara';
import { useState } from 'react';
function ServerDataTable() {
const [data, setData] = useState<User[]>([]);
const [isLoading, setIsLoading] = useState(false);
const handleQueryChange = async (query: DataTableQuery) => {
setIsLoading(true);
const response = await fetch('/api/users', {
method: 'POST',
body: JSON.stringify(query)
});
const result = await response.json();
setData(result.data);
setIsLoading(false);
};
return (
<DataTable
data={data}
columns={columns}
mode="server"
isLoading={isLoading}
onTableQueryChange={handleQueryChange}
defaultSort={{ name: 'name', order: 'asc' }}
>
<DataTable.Toolbar>
<DataTable.Search />
<DataTable.Filters />
</DataTable.Toolbar>
<DataTable.Content />
</DataTable>
);
}
With virtualization
Use VirtualizedContent for rendering large datasets efficiently.
import { DataTable } from '@raystack/apsara';
function VirtualizedTable() {
const largeDataset = Array.from({ length: 10000 }, (_, i) => ({
id: String(i),
name: `User ${i}`,
email: `user${i}@example.com`,
status: i % 2 === 0 ? 'active' : 'inactive'
}));
return (
<DataTable data={largeDataset} columns={columns} defaultSort={{ name: 'name', order: 'asc' }}>
<DataTable.Toolbar>
<DataTable.Search />
<DataTable.Filters />
</DataTable.Toolbar>
<DataTable.VirtualizedContent
rowHeight={48}
overscan={10}
/>
</DataTable>
);
}
Column definitions
Define columns with rich configuration options.
import { DataTableColumnDef } from '@raystack/apsara';
import { Badge } from '@raystack/apsara';
const columns: DataTableColumnDef<User, unknown>[] = [
{
accessorKey: 'name',
header: 'Name',
enableSorting: true,
enableColumnFilter: true,
filterType: 'text',
classNames: {
header: 'font-bold',
cell: 'text-gray-900'
},
cell: ({ getValue }) => {
return <span className="font-medium">{getValue() as string}</span>;
}
},
{
accessorKey: 'email',
header: 'Email',
enableSorting: true,
enableColumnFilter: true,
filterType: 'text'
},
{
accessorKey: 'status',
header: 'Status',
enableColumnFilter: true,
filterType: 'select',
filterOptions: [
{ label: 'Active', value: 'active' },
{ label: 'Inactive', value: 'inactive' }
],
cell: ({ getValue }) => {
const status = getValue() as string;
return (
<Badge variant={status === 'active' ? 'success' : 'neutral'}>
{status}
</Badge>
);
}
},
{
accessorKey: 'actions',
header: 'Actions',
enableSorting: false,
enableHiding: false,
cell: ({ row }) => {
return (
<Button
size="small"
variant="outline"
onClick={() => console.log('Edit', row.original)}
>
Edit
</Button>
);
}
}
];
Filtering
Enable column filters with various filter types.
const columns: DataTableColumnDef<Product, unknown>[] = [
{
accessorKey: 'name',
header: 'Name',
enableColumnFilter: true,
filterType: 'text' // Text input filter
},
{
accessorKey: 'price',
header: 'Price',
enableColumnFilter: true,
filterType: 'number', // Number input filter
dataType: 'number'
},
{
accessorKey: 'category',
header: 'Category',
enableColumnFilter: true,
filterType: 'select', // Dropdown select filter
filterOptions: [
{ label: 'Electronics', value: 'electronics' },
{ label: 'Clothing', value: 'clothing' },
{ label: 'Books', value: 'books' }
]
},
{
accessorKey: 'inStock',
header: 'In Stock',
enableColumnFilter: true,
filterType: 'boolean', // Boolean toggle filter
dataType: 'boolean'
},
{
accessorKey: 'releaseDate',
header: 'Release Date',
enableColumnFilter: true,
filterType: 'date', // Date picker filter
dataType: 'date'
}
];
Sorting
Enable sorting on columns.
const columns: DataTableColumnDef<User, unknown>[] = [
{
accessorKey: 'name',
header: 'Name',
enableSorting: true
},
{
accessorKey: 'createdAt',
header: 'Created',
enableSorting: true,
sortingFn: (rowA, rowB) => {
return new Date(rowA.original.createdAt).getTime() -
new Date(rowB.original.createdAt).getTime();
}
}
];
<DataTable
data={data}
columns={columns}
defaultSort={{ name: 'createdAt', order: 'desc' }}
>
<DataTable.Content />
</DataTable>
Grouping
Group rows by a column value.
const columns: DataTableColumnDef<Task, unknown>[] = [
{
accessorKey: 'status',
header: 'Status',
enableGrouping: true,
showGroupCount: true,
groupLabelsMap: {
'todo': 'To Do',
'in_progress': 'In Progress',
'done': 'Done'
}
},
{
accessorKey: 'title',
header: 'Title'
}
];
function GroupedTable() {
return (
<DataTable
data={tasks}
columns={columns}
defaultSort={{ name: 'title', order: 'asc' }}
query={{ group_by: ['status'] }}
>
<DataTable.Content />
</DataTable>
);
}
Column visibility
Control which columns are visible.
function TableWithColumnToggle() {
const [columnVisibility, setColumnVisibility] = useState({
email: true,
phone: false
});
return (
<DataTable
data={data}
columns={columns}
defaultSort={{ name: 'name', order: 'asc' }}
onColumnVisibilityChange={setColumnVisibility}
>
<DataTable.Toolbar>
<DataTable.DisplayControls />
</DataTable.Toolbar>
<DataTable.Content />
</DataTable>
);
}
Custom empty states
Customize empty and zero states.
import { EmptyState } from '@raystack/apsara';
import { MagnifyingGlassIcon, PlusIcon } from '@heroicons/react/24/outline';
function TableWithStates() {
return (
<DataTable data={data} columns={columns} defaultSort={{ name: 'name', order: 'asc' }}>
<DataTable.Toolbar>
<DataTable.Search />
<DataTable.Filters />
</DataTable.Toolbar>
<DataTable.Content
zeroState={
<EmptyState
icon={<PlusIcon />}
heading="No users yet"
subHeading="Get started by creating your first user"
primaryAction={<Button>Create User</Button>}
/>
}
emptyState={
<EmptyState
icon={<MagnifyingGlassIcon />}
heading="No results found"
subHeading="Try adjusting your search or filters"
/>
}
/>
</DataTable>
);
}
Row click handling
Handle row clicks for navigation or actions.
import { useRouter } from 'next/router';
function ClickableTable() {
const router = useRouter();
return (
<DataTable
data={data}
columns={columns}
defaultSort={{ name: 'name', order: 'asc' }}
onRowClick={(row) => {
router.push(`/users/${row.id}`);
}}
>
<DataTable.Content />
</DataTable>
);
}
Using the useDataTable hook
Access table context from child components.
import { useDataTable } from '@raystack/apsara';
function CustomToolbarButton() {
const { table, tableQuery, updateTableQuery } = useDataTable();
const clearFilters = () => {
updateTableQuery((prev) => ({
...prev,
filters: []
}));
};
return (
<Button onClick={clearFilters}>
Clear All Filters
</Button>
);
}
function TableWithCustomButton() {
return (
<DataTable data={data} columns={columns} defaultSort={{ name: 'name', order: 'asc' }}>
<DataTable.Toolbar>
<DataTable.Filters />
<CustomToolbarButton />
</DataTable.Toolbar>
<DataTable.Content />
</DataTable>
);
}
Infinite loading
Implement infinite scroll loading for server-side data.
function InfiniteTable() {
const [data, setData] = useState<User[]>([]);
const [hasMore, setHasMore] = useState(true);
const loadMore = async () => {
if (!hasMore) return;
const response = await fetch(`/api/users?offset=${data.length}`);
const newData = await response.json();
setData([...data, ...newData]);
setHasMore(newData.length > 0);
};
return (
<DataTable
data={data}
columns={columns}
mode="server"
defaultSort={{ name: 'name', order: 'asc' }}
onLoadMore={loadMore}
>
<DataTable.Content />
</DataTable>
);
}
Custom styling
Apply custom styles to table elements.
function StyledTable() {
return (
<DataTable data={data} columns={columns} defaultSort={{ name: 'name', order: 'asc' }}>
<DataTable.Content
classNames={{
root: 'custom-table-root',
table: 'custom-table',
header: 'custom-header',
body: 'custom-body',
row: 'custom-row'
}}
/>
</DataTable>
);
}
API reference
DataTable (root)
Array of data objects to display in the table.
columns
DataTableColumnDef<TData, TValue>[]
required
Column definitions for the table.
Default sort configuration with name (column accessor) and order (‘asc’ | ‘desc’).
mode
'client' | 'server'
default:"'client'"
Data fetching mode. Use ‘server’ for backend pagination and filtering.
Initial query state for filters, sorting, grouping, and pagination.
Whether data is currently loading.
Number of skeleton rows to show while loading.
onTableQueryChange
(query: DataTableQuery) => void
Callback fired when the query changes (server mode only).
Callback for infinite scroll loading (server mode only).
Callback fired when a row is clicked.
onColumnVisibilityChange
(columnVisibility: VisibilityState) => void
Callback fired when column visibility changes.
DataTableColumnDef
Key to access the data in each row object.
Column header text or render function.
Custom cell render function.
Whether sorting is enabled for this column.
Whether filtering is enabled for this column.
Whether the column can be hidden.
Whether the column is hidden by default.
filterType
'text' | 'number' | 'select' | 'boolean' | 'date'
Type of filter input to use.
dataType
'string' | 'number' | 'boolean' | 'date'
Data type for type-specific filtering.
Options for select-type filters. Each option has label and value.
Whether the column can be used for grouping.
Whether to show count badge in group headers.
Map of data values to display labels for group headers.
classNames
{ header?: string; cell?: string }
Custom CSS classes for header and cell elements.
styles
{ header?: React.CSSProperties; cell?: React.CSSProperties }
Inline styles for header and cell elements.
DataTable.Content
Component to show when no data matches the current filters/search.
Component to show when there is no data and no filters are applied.
classNames
{ root?: string; table?: string; header?: string; body?: string; row?: string }
Custom CSS classes for various table elements.
DataTable.VirtualizedContent
Extends DataTable.Content props with additional virtualization options.
Height of each row in pixels.
Height of group header rows in pixels. Falls back to rowHeight if not set.
Number of rows to render outside the visible area for smooth scrolling.
Distance in pixels from bottom to trigger onLoadMore.
DataTable.Toolbar
Container for toolbar elements like search, filters, and display controls.
DataTable.Search
Placeholder text for the search input.
DataTable.Filters
Renders filter controls for columns with enableColumnFilter: true.
DataTable.DisplayControls
Renders column visibility controls and other display settings.
useDataTable hook
Hook to access the data table context from child components.
Returns:
TanStack Table instance with full API access.
columns
DataTableColumnDef<TData, TValue>[]
Column definitions.
Current table query state including filters, sorting, and pagination.
updateTableQuery
(fn: (query: InternalQuery) => InternalQuery) => void
Function to update the table query.
Current data fetching mode.
Whether data is currently loading.
Function to reset display settings to defaults.