Ultra-light, registry-based data table for React + TanStack Table + TanStack Query.
- Zero heavy dependencies: Only
@tanstack/react-queryand@tanstack/react-tableas peer dependencies - Registry-based: Inject your own i18n and Link component
- TypeScript: Full type support with generics
- Two modes: Client-side and Server-side pagination/filtering/sorting
- Customizable: Override styles via CSS variables
npm install @tanstack/react-query @tanstack/react-table
npm install @snowpact/react-tanstack-query-table// In your app entry point (main.tsx or App.tsx)
import '@snowpact/react-tanstack-query-table/styles.css';// In your app entry point (main.tsx or App.tsx)
import { setupSnowTable } from '@snowpact/react-tanstack-query-table';
import { Link } from 'react-router-dom';
import { t } from './i18n'; // Your translation function
setupSnowTable({
translate: (key) => t(key),
LinkComponent: Link,
});Translation keys:
- Dynamic keys (column labels, etc.) - Your
translatefunction handles these - Static UI keys (
dataTable.*) - Built-in English defaults iftranslatereturns the key unchanged
| Key | Default |
|---|---|
dataTable.search |
"Search..." |
dataTable.elements |
"elements" |
dataTable.paginationSize |
"per page" |
dataTable.columnsConfiguration |
"Columns" |
dataTable.resetFilters |
"Reset filters" |
dataTable.resetColumns |
"Reset" |
dataTable.searchFilters |
"Search..." |
dataTable.searchEmpty |
"No results found" |
dataTable.selectFilter |
"Select..." |
Override static keys without i18n:
setupSnowTable({
translate: (key) => key,
LinkComponent: Link,
translations: { 'dataTable.search': 'Rechercher...' },
});import { SnowClientDataTable, SnowColumnConfig } from '@snowpact/react-tanstack-query-table';
type User = { id: string; name: string; email: string; status: string };
const columns: SnowColumnConfig<User>[] = [
{ key: 'name', label: 'Name' },
{ key: 'email', label: 'Email' },
{ key: 'status', label: 'Status', render: (item) => <Badge>{item.status}</Badge> },
];
<SnowClientDataTable
queryKey={['users']}
fetchAllItemsEndpoint={() => fetchUsers()}
columnConfig={columns}
enableGlobalSearch
enablePagination
enableSorting
enableColumnConfiguration
defaultPageSize={20}
defaultSortBy="name"
defaultSortOrder="asc"
persistState
/>That's it! You have a working data table.
Override CSS variables to match your design:
:root {
--snow-background: #ffffff;
--snow-foreground: #0a0a0a;
--snow-secondary: #f5f5f5;
--snow-secondary-foreground: #737373;
--snow-border: #d4d4d4;
--snow-ring: #a3a3a3;
--snow-radius: 0.375rem;
}
/* Dark mode */
.dark {
--snow-background: #1a1a2e;
--snow-foreground: #eaeaea;
--snow-secondary: #16213e;
--snow-secondary-foreground: #a0a0a0;
--snow-border: #0f3460;
--snow-ring: #3b82f6;
}| Mode | Component | Use case | Data handling |
|---|---|---|---|
| Client | SnowClientDataTable |
< 5,000 items | All data loaded, filtered/sorted locally |
| Server | SnowServerDataTable |
> 5,000 items | Server handles pagination/filtering |
Fetches all data once, handles everything in the browser:
<SnowClientDataTable
queryKey={['users']}
fetchAllItemsEndpoint={() => api.getUsers()}
columnConfig={columns}
/>Server handles pagination, search, filtering, and sorting:
import { SnowServerDataTable, ServerFetchParams } from '@snowpact/react-tanstack-query-table';
const fetchUsers = async (params: ServerFetchParams) => {
// params: { limit, offset, search?, sortBy?, sortOrder?, filters?, prefilter? }
const response = await api.getUsers(params);
return {
items: response.data,
totalItemCount: response.total,
};
};
<SnowServerDataTable
queryKey={['users']}
fetchServerEndpoint={fetchUsers}
columnConfig={columns}
/>Actions appear as buttons in each row:
{
type: 'click',
icon: EditIcon,
label: 'Edit',
onClick: (item) => openEditModal(item),
}{
type: 'link',
icon: EyeIcon,
label: 'View',
href: (item) => `/users/${item.id}`,
external: false, // true for target="_blank"
}For API calls with built-in mutation handling:
{
type: 'endpoint',
icon: TrashIcon,
label: 'Delete',
variant: 'danger',
endpoint: (item) => api.deleteUser(item.id),
onSuccess: () => {
toast.success('User deleted');
queryClient.invalidateQueries(['users']);
},
onError: (error) => toast.error(error.message),
}Use withConfirm to show a confirmation dialog before the endpoint is called:
{
type: 'endpoint',
icon: TrashIcon,
label: 'Delete',
variant: 'danger',
endpoint: (item) => api.deleteUser(item.id),
withConfirm: async (item) => {
// Return true to proceed, false to cancel
return window.confirm(`Delete ${item.name}?`);
// Or use your own dialog library (e.g., sweetalert2, radix-ui/dialog)
},
onSuccess: () => queryClient.invalidateQueries(['users']),
}The endpoint is only called if withConfirm returns true (or a truthy Promise).
actions={[
(item) => ({
type: 'click',
icon: item.isActive ? PauseIcon : PlayIcon,
label: item.isActive ? 'Deactivate' : 'Activate',
onClick: () => toggleStatus(item),
hidden: item.role === 'admin',
}),
]}<SnowClientDataTable
enableGlobalSearch
texts={{ searchPlaceholder: 'Search users...' }}
/><SnowClientDataTable
filters={[
{
key: 'status',
label: 'Status',
multipleSelection: true, // Allow multiple values
options: [
{ value: 'active', label: 'Active' },
{ value: 'inactive', label: 'Inactive' },
],
},
{
key: 'role',
label: 'Role',
options: [
{ value: 'admin', label: 'Admin' },
{ value: 'user', label: 'User' },
],
},
]}
/><SnowClientDataTable
prefilters={[
{ id: 'all', label: 'All' },
{ id: 'active', label: 'Active' },
]}
prefilterFn={(item, prefilterId) => {
if (prefilterId === 'all') return true;
return item.status === prefilterId;
}}
/><SnowClientDataTable persistState />Saves pagination, search, filters, and sorting in URL params.
<SnowClientDataTable
enableColumnConfiguration
columnConfig={[
{ key: 'name' },
{ key: 'details', meta: { defaultHidden: true } },
]}
/><SnowClientDataTable
enableSorting
defaultSortBy="createdAt"
defaultSortOrder="desc"
/><SnowClientDataTable
onRowClick={(item) => navigate(`/users/${item.id}`)}
activeRowId={selectedUserId}
/>const columns: SnowColumnConfig<User>[] = [
{ key: 'name', label: 'Name' },
{
key: 'status',
label: 'Status',
render: (item) => (
<span className={item.status === 'active' ? 'text-green-500' : 'text-red-500'}>
{item.status}
</span>
),
},
{
key: '_extra_fullName', // Use _extra_ prefix for computed columns
label: 'Full Name',
render: (item) => `${item.firstName} ${item.lastName}`,
searchableValue: (item) => `${item.firstName} ${item.lastName}`,
},
];Use meta to customize column appearance and behavior:
import { SnowColumnConfig, SnowColumnMeta } from '@snowpact/react-tanstack-query-table';
const columns: SnowColumnConfig<User>[] = [
{
key: 'id',
label: 'ID',
meta: {
width: '80px',
center: true,
},
},
{
key: 'name',
label: 'Name',
meta: {
minWidth: '150px',
maxWidth: '300px',
},
},
{
key: 'description',
label: 'Description',
meta: {
defaultHidden: true, // Hidden by default in column configuration
},
},
{
key: 'actions',
label: '',
meta: {
width: 'auto',
disableColumnClick: true, // Don't trigger onRowClick for this column
},
},
];| Option | Type | Description |
|---|---|---|
width |
string | number |
Column width (e.g., '200px', '20%', 'auto') |
minWidth |
string | number |
Minimum column width |
maxWidth |
string | number |
Maximum column width |
defaultHidden |
boolean |
Hide column by default (with enableColumnConfiguration) |
disableColumnClick |
boolean |
Disable onRowClick for this column |
center |
boolean |
Center column content |
| Prop | Type | Default | Description |
|---|---|---|---|
queryKey |
string[] |
Required | React Query cache key |
fetchAllItemsEndpoint |
() => Promise<T[]> |
Required | Data fetching function |
columnConfig |
SnowColumnConfig<T>[] |
Required | Column definitions |
actions |
TableAction<T>[] |
- | Row actions |
filters |
FilterConfig<T>[] |
- | Column filters |
prefilters |
PreFilter[] |
- | Tab filters |
prefilterFn |
(item, id) => boolean |
- | Client-side prefilter logic |
persistState |
boolean |
false |
Persist state in URL |
enableGlobalSearch |
boolean |
false |
Enable search bar |
enablePagination |
boolean |
true |
Enable pagination |
enableSorting |
boolean |
true |
Enable column sorting |
enableColumnConfiguration |
boolean |
false |
Enable column visibility toggle |
defaultPageSize |
number |
10 |
Initial page size |
defaultSortBy |
string |
- | Initial sort column |
defaultSortOrder |
'asc' | 'desc' |
'asc' |
Initial sort direction |
Same as SnowClientDataTable, plus:
| Prop | Type | Description |
|---|---|---|
fetchServerEndpoint |
(params: ServerFetchParams) => Promise<ServerPaginatedResponse<T>> |
Paginated fetch function |
interface ServerFetchParams {
limit: number;
offset: number;
search?: string;
prefilter?: string;
filters?: Record<string, string[]>;
sortBy?: string;
sortOrder?: 'ASC' | 'DESC';
}interface ServerPaginatedResponse<T> {
items: T[];
totalItemCount: number;
}MIT