A fully declarative, type-safe React table component built with TanStack Table, Zod, and Next.js. Create powerful, feature-rich data tables with minimal code.
See it in action with all features enabled!
-
100% Declarative API - Define your table with just a Zod schema and column definitions
<DataTable schema={userSchema} data={users} columns={columns} />
That's it! No complex setup or boilerplate needed.
-
Type-Safe with Zod - Full TypeScript support with runtime validation
- Runtime validation ensures data integrity
- Compile-time type safety catches errors early
- Filter values automatically parsed based on schema types
-
URL Query Parameter Sync - Every aspect of table state is synchronized to the URL
/users?page=2&pageSize=50&filter=admin&filterColumn=role&sortBy=createdAt&sortOrder=desc- Bookmarkable table states
- Shareable links with filters/sorts applied
- Browser back/forward navigation works
- Deep linking to specific views
-
Virtual Scrolling - Handle massive datasets with ease via
@tanstack/react-virtual- Render only visible rows (not all 10,000+)
- Smooth scrolling performance
- Minimal DOM nodes
- Auto-measured row heights
-
Advanced Filtering - Custom filter components per column with automatic type parsing
z.string()→ Text inputz.number()→ Number validationz.boolean()→ Boolean selectz.date()→ Date picker with proper parsing- Custom filter functions for complex logic
-
Three-State Sorting - Click column headers to cycle through:
- None → No sorting
- Ascending → A-Z, 0-9, oldest-newest
- Descending → Z-A, 9-0, newest-oldest
- Back to None
-
Bulk Actions - Select multiple rows and perform batch operations
- Row selection with checkboxes
- Select all on current page
- Confirmation dialogs for destructive actions
- Async action support with loading states
- Custom icons and styling
-
Column Visibility - Show/hide columns dynamically
- Persists in URL query params
- Checkbox list of all columns
- Quick show/hide toggle
- Some columns can be locked (always visible)
-
Column Reordering - Drag & drop columns to reorder (opt-in per column via
meta.enableColumnOrdering)- Visual grip icon for draggable columns
- Smooth drag & drop animations
- Works with filtering, sorting, and pagination
- Keyboard and touch support
-
Row Reordering - Drag & drop rows to change order (opt-in via
enableRowOrderingprop)- Visual grip icon on each row
- Smooth drag & drop animations
- Works with virtual scrolling
- Perfect for task lists, playlists, menus
-
Responsive Design - Automatically adapts to mobile devices
- Desktop: Full toolbar with all controls
- Mobile: Dropdown menus for actions and settings
- Touch-friendly tap targets
- Virtual scrolling works on mobile
-
Pagination - Built-in pagination with customizable page sizes
- Customizable page sizes (default: 15, 50, 100, 250, 500)
- Page number input with validation
- First/previous/next/last navigation
- Total row count display
- Selected row count
-
Auto-scroll - Optional auto-scroll to top when changing pages
- Enabled by default
- Toggle in settings menu
- Smooth scroll animation
- Persists preference in URL
-
Zero Config - Works out of the box with sensible defaults
- Only configure what you need to customize
-
TypeScript First - Fully typed with TypeScript 5+
- Generic type parameters
- Inferred types from Zod schemas
- Type-safe column definitions
- No
anytypes in public APIs
-
Performance Optimized
- Virtual scrolling for large datasets
- Memoized column definitions
- Efficient filtering/sorting algorithms
- Minimal re-renders
-
Extensible
- Custom filter components
- Custom sort functions
- Custom cell renderers
- Custom bulk actions
- Custom header components
-
Well Documented
- Comprehensive README
- API reference
- Multiple examples
- Inline code comments
Tested with:
- 10 rows: Instant
- 100 rows: < 50ms render
- 1,000 rows: < 100ms render
- 10,000 rows: < 200ms render (virtual scrolling)
- 100,000 rows: < 500ms render (virtual scrolling)
Filtering and sorting remain fast even with large datasets.
# Core dependencies
npm install zod @tanstack/react-table @tanstack/react-virtual @tanstack/react-pacer nuqs @dnd-kit/core @dnd-kit/sortable @dnd-kit/utilities lucide-react
# shadcn/ui components (required)
npx shadcn@latest init
npx shadcn@latest add button input label select table tabs dropdown-menu switch skeleton
# Or using pnpm
pnpm add zod @tanstack/react-table @tanstack/react-virtual @tanstack/react-pacer nuqs @dnd-kit/core @dnd-kit/sortable @dnd-kit/utilities lucide-react
# Or using yarn
yarn add zod @tanstack/react-table @tanstack/react-virtual @tanstack/react-pacer nuqs @dnd-kit/core @dnd-kit/sortable @dnd-kit/utilities lucide-reactSee INSTALL.md for complete installation instructions.
import { DataTable } from "@/components/table/DataTable";
import { ColumnConfig } from "@/lib/table/columnConfig";
import { z } from "zod";
// 1. Define your schema
const userSchema = z.object({
id: z.number(),
name: z.string(),
email: z.string(),
role: z.string(),
createdAt: z.string(),
});
// 2. Define your columns using ColumnConfig
const columns =
ColumnConfig <
userSchema >
[]([
{
accessorKey: "name",
header: "Name",
},
{
accessorKey: "email",
header: "Email",
},
{
accessorKey: "role",
header: "Role",
},
]);
// 3. Use the table
export default function UsersPage() {
const users = [
{
id: 1,
name: "John Doe",
email: "john@example.com",
role: "Admin",
createdAt: "2024-01-01",
},
{
id: 2,
name: "Jane Smith",
email: "jane@example.com",
role: "User",
createdAt: "2024-01-02",
},
];
return <DataTable schema={userSchema} data={users} columns={columns} />;
}That's it! You now have a fully functional table with filtering, sorting, and pagination.
Add custom filter UI for each column via the meta property:
import { Input } from "@/components/ui/input";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import type { FilterComponentProps } from "@/lib/table/types";
import { ColumnConfig } from "@/lib/table/columnConfig";
const columns =
ColumnConfig <
userSchema >
[]([
{
accessorKey: "name",
header: "Name",
meta: {
FilterComponent: ({
value,
onChange,
label,
}: FilterComponentProps<string>) => (
<Input
placeholder={label}
value={value}
onChange={(e) => onChange(e.target.value)}
/>
),
filterLabel: "Search by name",
},
},
{
accessorKey: "role",
header: "Role",
meta: {
FilterComponent: ({
value,
onChange,
label,
}: FilterComponentProps<string>) => (
<Select
value={value || "all"}
onValueChange={(val) => onChange(val === "all" ? "" : val)}
>
<SelectTrigger>
<SelectValue placeholder={label} />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All</SelectItem>
<SelectItem value="admin">Admin</SelectItem>
<SelectItem value="user">User</SelectItem>
</SelectContent>
</Select>
),
filterLabel: "Filter by role",
},
},
]);Enable row selection and batch operations:
import { BulkAction } from "@/lib/table/types";
import { Trash2, Shield } from "lucide-react";
const bulkActions: BulkAction<User>[] = [
{
label: "Delete Users",
variant: "destructive",
icon: Trash2,
confirmMessage: "Are you sure you want to delete {count} users?",
action: async (rows) => {
const userIds = rows.map((r) => r.original.id);
await deleteUsers(userIds);
},
},
{
label: "Make Admin",
variant: "default",
icon: Shield,
confirmMessage: "Promote {count} users to admin?",
action: async (rows) => {
const userIds = rows.map((r) => r.original.id);
await promoteToAdmin(userIds);
},
},
];
const columns = CreateColumnConfig(userSchema, [
// ...column definitions
]);
<DataTable
schema={userSchema}
data={users}
columns={columns}
bulkActions={bulkActions}
/>;Use the SortableHeader component for sortable columns:
import { SortableHeader } from "@/components/table/SortableHeader";
import { ColumnConfig } from "@/lib/table/columnConfig";
const columns =
ColumnConfig <
schema >
[]([
{
key: "name",
header: ({ column, table }) => (
<SortableHeader column={column} table={table}>
Name
</SortableHeader>
),
enableSorting: true,
},
// ...other columns
]);Add action buttons to each row:
import { ColumnConfig } from "@/lib/table/columnConfig";
const columns =
ColumnConfig <
schema >
[]([
// ... other columns
{
id: "actions",
header: () => <div className="text-right">Actions</div>,
cell: ({ row }) => (
<div className="flex justify-end gap-2">
<Button onClick={() => editUser(row.original.id)}>Edit</Button>
<Button
variant="destructive"
onClick={() => deleteUser(row.original.id)}
>
Delete
</Button>
</div>
),
enableSorting: false,
enableHiding: false,
},
]);The library automatically parses date values from the schema:
import { DatePicker } from "@/components/ui/date-picker";
import { ColumnConfig } from "@/lib/table/columnConfig";
const userSchema = z.object({
createdAt: z.string(), // or z.date()
// ... other fields
});
const columns =
ColumnConfig <
userSchema >
[]([
{
key: "createdAt",
header: "Created At",
filterFn: (row, columnId, filterValue) => {
if (!filterValue) return true;
const cellValue = new Date(row.getValue(columnId) as string);
const filterDate = new Date(filterValue);
return (
cellValue.getFullYear() === filterDate.getFullYear() &&
cellValue.getMonth() === filterDate.getMonth() &&
cellValue.getDate() === filterDate.getDate()
);
},
filterComponent: ({
parsedValue,
onChange,
label,
}: FilterComponentProps<Date>) => (
<DatePicker
date={parsedValue || undefined}
onDateChange={(date) => onChange(date ? date.toISOString() : "")}
placeholder={label}
/>
),
},
]);<DataTable
schema={userSchema}
data={users}
columns={columns}
limits={[10, 25, 50, 100]}
/><DataTable
schema={userSchema}
data={users}
columns={columns}
enableFilter={false}
/>| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
schema |
z.ZodObject<any> |
Yes | - | Zod schema for data validation and type inference |
data |
z.infer<Schema>[] |
Yes | - | Array of data to display |
columns |
<typeof ColumnConfig> |
Yes | - | Column definitions generated by ColumnConfig |
bulkActions |
BulkAction<z.infer<Schema>>[] |
No | undefined |
Bulk actions for selected rows |
limits |
number[] |
No | [15, 50, 100, 250, 500] |
Available page size options |
enableFilter |
boolean |
No | true |
Enable/disable column filtering |
TableActions |
(table) => ReactNode |
No | undefined |
Custom actions component |
interface BulkAction<TData> {
label: string;
action: (rows: Row<TData>[]) => void | Promise<void>;
variant?: "default" | "destructive";
confirmMessage?: string; // Use {count} placeholder for number of selected rows
icon?: React.ComponentType<{ className?: string }>;
}interface FilterComponentProps<T = unknown> {
value: string; // Current filter value (from URL)
onChange: (value: string) => void; // Update filter value
label: string; // Filter label from column meta
parsedValue: T | null; // Parsed value based on schema type
schemaType: "string" | "boolean" | "number" | "date" | "unknown";
}When you define bulkActions in the DataTable component, you can set withBulkActions to true in the CreateColumnConfig function. A select (checkbox) column is automatically inserted as the first column, even if you do not explicitly define it. If you want to override the default select column (e.g., to customize its header, cell, or behavior), simply define a column with id: "select" in your column config—your definition will take precedence.
Similarly, when you enable row ordering (by setting enableRowOrdering), a drag handle column is automatically inserted as the first column (before select, if present). You can override the default drag column by defining a column with id: "drag".
Example:
const columnsConfig =
ColumnConfig <
schema >
[]([
{
id: "drag", // Custom drag column (overrides default)
// ...custom config
},
{
id: "select", // Custom select column (overrides default)
// ...custom config
},
// ...other columns
]);
const columns = useMemo(
() =>
CreateColumnConfig<schema>(columnsConfig, {
withBulkActions: !!bulkActions,
enableRowOrdering: false,
}),
[bulkActions]
);If you do not define these columns, the defaults will be inserted automatically when the relevant features are enabled.
The table automatically syncs state to URL query parameters:
page- Current page numberpageSize- Number of rows per pagefilter- Current filter valuefilterColumn- Column being filteredsortBy- Column being sortedsortOrder- Sort direction (ascordesc)autoScroll- Auto-scroll to top on page change (trueorfalse)
Example URL:
/users?page=2&pageSize=50&filter=admin&filterColumn=role&sortBy=name&sortOrder=asc
- DataTable - Main table component that orchestrates all features
- VirtualTable - Virtual scrolling implementation
- TablePagination - Pagination controls
- TableFilter - Column filtering UI
- BulkActions - Bulk action buttons
- SortableHeader - Sortable column headers
- ColumnVisibilityDropdown - Show/hide columns
- TableSettings - Additional table settings
- useDataTable - Main hook managing table state and URL sync
The library uses Zod schemas to:
- Validate data structure
- Infer TypeScript types
- Parse filter values based on field types
- Provide type-safe column definitions
- React 18+
- Next.js 13+ (App Router)
- TypeScript 5+
{
"zod": "^3.x",
"@tanstack/react-table": "^8.x",
"@tanstack/react-virtual": "^3.x",
"nuqs": "^1.x"
}- Chrome (latest)
- Firefox (latest)
- Safari (latest)
- Edge (latest)
Check out the example directory for a complete implementation:
- page.tsx - Complete page example with columns definition and custom filtering components
Contributions are welcome! Please read our Contributing Guide for details.
MIT License - see LICENSE file for details.
Built with:
- TanStack Table - Headless table library
- TanStack Virtual - Virtual scrolling
- TanStack Pacer - Debounced URL Syncing
- Zod - Schema validation
- nuqs - Type-safe query string state
Make sure:
- Your column has
enableColumnFilterset totrue(or not set tofalse) - Column has a valid
accessorKeyorid - Filter value matches the schema type
Ensure:
- Column has
enableSorting: true - You're using the
SortableHeadercomponent - Column has a valid
accessorKey
Verify:
- Your data matches the Zod schema
- Column definitions use
z.infer<typeof schema>as the generic type - All dependencies are up to date
Perfect for:
- Admin dashboards
- User management interfaces
- Product catalogs
- Order management systems
- Analytics dashboards
- Content management systems
- Any data-heavy application
This library focuses on client-side table features. It does NOT include:
- Server-side pagination/filtering/sorting
- Data fetching/caching
- Form editing / Inline row editing
- Tree/hierarchical data
- Column resizing
- Row grouping
- Excel/PDF export
Many of these can be added on top of the library if needed.
Made for the Next.js community