@venkateshsirigineedi/bolt-table
v0.1.0
Published
Virtualized React table with column drag & drop, pinning, resizing, sorting, filtering, and pagination.
Downloads
59
Readme
@venkateshsirigineedi/bolt-table
A high-performance, fully-featured React table component built on TanStack Virtual and @dnd-kit. Only the rows visible in the viewport are ever in the DOM — making it fast for datasets of any size.
Features
- Row virtualization — only visible rows are rendered, powered by TanStack Virtual
- Drag to reorder columns — grab any header and drag it to a new position
- Column pinning — pin columns to the left or right edge via right-click
- Column resizing — drag the right edge of any header to resize
- Column hiding — hide/show columns via the right-click context menu
- Sorting — client-side or server-side, with custom comparators per column
- Filtering — client-side or server-side, with custom filter functions per column
- Pagination — client-side slice or server-side with full control
- Row selection — checkbox or radio, with select-all, indeterminate state, and disabled rows
- Expandable rows — auto-measured content panels below each row, controlled or uncontrolled
- Shimmer loading — animated skeleton rows on initial load and infinite scroll append
- Infinite scroll —
onEndReachedcallback with configurable threshold - Empty state — custom renderer or default "No data" message
- Auto height — table shrinks/grows to fit rows, capped at 10 rows by default
- Right-click context menu — sort, filter, pin, hide, plus custom items
- Dark mode — works out of the box with CSS variables
Installation
npm install @venkateshsirigineedi/bolt-tablePeer dependencies
These must be installed separately in your project:
npm install @tanstack/react-virtual @dnd-kit/core @dnd-kit/sortable lucide-reactQuick start
import { BoltTable, ColumnType } from '@venkateshsirigineedi/bolt-table';
interface User {
id: string;
name: string;
email: string;
age: number;
}
const columns: ColumnType<User>[] = [
{ key: 'name', dataIndex: 'name', title: 'Name', width: 200 },
{ key: 'email', dataIndex: 'email', title: 'Email', width: 280 },
{ key: 'age', dataIndex: 'age', title: 'Age', width: 80 },
];
const data: User[] = [
{ id: '1', name: 'Alice', email: '[email protected]', age: 28 },
{ id: '2', name: 'Bob', email: '[email protected]', age: 34 },
{ id: '3', name: 'Charlie', email: '[email protected]', age: 22 },
];
export default function App() {
return (
<BoltTable<User>
columns={columns}
data={data}
rowKey="id"
/>
);
}Next.js (App Router)
BoltTable uses browser APIs and must be wrapped in a client boundary. Remove the 'use client' directive from the component files and wrap usage instead:
'use client';
import { BoltTable } from '@venkateshsirigineedi/bolt-table';Styling
BoltTable uses Tailwind CSS utility classes and Shadcn/ui CSS variables (--muted, --background, --border, etc.).
Make sure your project has Tailwind configured and the Shadcn CSS variables defined in your global stylesheet. If you use a different design system, you can override styles via the styles and classNames props.
Props
BoltTable
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| columns | ColumnType<T>[] | — | Column definitions (required) |
| data | T[] | — | Row data array (required) |
| rowKey | string \| (record: T) => string | 'id' | Unique row identifier |
| rowHeight | number | 40 | Height of each row in pixels |
| expandedRowHeight | number | 200 | Estimated height for expanded rows |
| maxExpandedRowHeight | number | — | Max height for expanded row panels (makes them scrollable) |
| accentColor | string | '#1890ff' | Color used for sort icons, selected rows, resize line, etc. |
| className | string | '' | Class name for the outer wrapper |
| classNames | ClassNamesTypes | {} | Granular class overrides per table region |
| styles | StylesTypes | {} | Inline style overrides per table region |
| gripIcon | ReactNode | — | Custom drag grip icon (defaults to GripVertical) |
| hideGripIcon | boolean | false | Hide the drag grip icon from all headers |
| pagination | PaginationType \| false | — | Pagination config, or false to disable |
| onPaginationChange | (page, pageSize) => void | — | Called when page or page size changes |
| onColumnResize | (columnKey, newWidth) => void | — | Called when a column is resized |
| onColumnOrderChange | (newOrder) => void | — | Called when columns are reordered |
| onColumnPin | (columnKey, pinned) => void | — | Called when a column is pinned/unpinned |
| onColumnHide | (columnKey, hidden) => void | — | Called when a column is hidden/shown |
| rowSelection | RowSelectionConfig<T> | — | Row selection config |
| expandable | ExpandableConfig<T> | — | Expandable row config |
| onEndReached | () => void | — | Called when scrolled near the bottom (infinite scroll) |
| onEndReachedThreshold | number | 5 | Rows from end to trigger onEndReached |
| isLoading | boolean | false | Shows shimmer skeleton rows when true |
| onSortChange | (columnKey, direction) => void | — | Server-side sort handler (disables local sort) |
| onFilterChange | (filters) => void | — | Server-side filter handler (disables local filter) |
| columnContextMenuItems | ColumnContextMenuItem[] | — | Custom items appended to the header context menu |
| autoHeight | boolean | true | Auto-size table height to content (capped at 10 rows) |
| layoutLoading | boolean | false | Show full skeleton layout (headers + rows) |
| emptyRenderer | ReactNode | — | Custom empty state content |
ColumnType<T>
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| key | string | — | Unique column identifier (required) |
| dataIndex | string | — | Row object property to display (required) |
| title | string \| ReactNode | — | Header label (required) |
| width | number | 150 | Column width in pixels |
| render | (value, record, index) => ReactNode | — | Custom cell renderer |
| shimmerRender | () => ReactNode | — | Custom shimmer skeleton for this column |
| sortable | boolean | true | Show sort controls for this column |
| sorter | boolean \| (a: T, b: T) => number | — | Custom sort comparator for client-side sort |
| filterable | boolean | true | Show filter option in context menu |
| filterFn | (value, record, dataIndex) => boolean | — | Custom filter predicate for client-side filter |
| hidden | boolean | false | Hide this column |
| pinned | 'left' \| 'right' \| false | false | Pin this column to an edge |
| className | string | — | Class applied to all cells in this column |
| style | CSSProperties | — | Styles applied to all cells in this column |
Examples
Sorting
Client-side (no onSortChange — BoltTable sorts locally):
const columns: ColumnType<User>[] = [
{
key: 'name',
dataIndex: 'name',
title: 'Name',
sortable: true,
// Optional custom comparator:
sorter: (a, b) => a.name.localeCompare(b.name),
},
{
key: 'age',
dataIndex: 'age',
title: 'Age',
sortable: true,
// Default numeric comparator used when sorter is omitted
},
];
<BoltTable columns={columns} data={data} />Server-side (provide onSortChange — BoltTable delegates to you):
const [sortKey, setSortKey] = useState('');
const [sortDir, setSortDir] = useState<SortDirection>(null);
<BoltTable
columns={columns}
data={serverData}
onSortChange={(key, dir) => {
setSortKey(key);
setSortDir(dir);
refetch({ sortKey: key, sortDir: dir });
}}
/>Filtering
Client-side (no onFilterChange):
const columns: ColumnType<User>[] = [
{
key: 'status',
dataIndex: 'status',
title: 'Status',
filterable: true,
// Exact match instead of default substring:
filterFn: (value, record) => record.status === value,
},
];Server-side:
<BoltTable
columns={columns}
data={serverData}
onFilterChange={(filters) => {
setActiveFilters(filters);
refetch({ filters });
}}
/>Pagination
Client-side (pass all data, BoltTable slices it):
<BoltTable
columns={columns}
data={allUsers} // all 500 rows
pagination={{ pageSize: 20 }}
onPaginationChange={(page, size) => {
setPage(page);
}}
/>Server-side (pass only the current page):
<BoltTable
columns={columns}
data={currentPageData} // only 20 rows
pagination={{
current: page,
pageSize: 20,
total: 500,
showTotal: (total, [from, to]) => `${from}-${to} of ${total} users`,
}}
onPaginationChange={(page, size) => fetchPage(page, size)}
/>Disable pagination:
<BoltTable columns={columns} data={data} pagination={false} />Row selection
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
<BoltTable
columns={columns}
data={data}
rowKey="id"
rowSelection={{
type: 'checkbox', // or 'radio'
selectedRowKeys,
onChange: (keys, rows) => setSelectedRowKeys(keys),
// Disable selection for specific rows:
getCheckboxProps: (record) => ({
disabled: record.status === 'locked',
}),
}}
/>Expandable rows
<BoltTable
columns={columns}
data={data}
rowKey="id"
expandable={{
rowExpandable: (record) => record.details !== null,
expandedRowRender: (record) => (
<div style={{ padding: 16 }}>
<h4>{record.name} — Details</h4>
<pre>{JSON.stringify(record.details, null, 2)}</pre>
</div>
),
// Optional: control expanded state yourself
// expandedRowKeys={expandedKeys}
// onExpandedRowsChange={(keys) => setExpandedKeys(keys)}
}}
expandedRowHeight={150} // initial estimate
maxExpandedRowHeight={400} // makes panel scrollable if taller
/>Infinite scroll
const [data, setData] = useState<User[]>([]);
const [isLoading, setIsLoading] = useState(false);
const loadMore = async () => {
setIsLoading(true);
const newRows = await fetchNextPage();
setData(prev => [...prev, ...newRows]);
setIsLoading(false);
};
<BoltTable
columns={columns}
data={data}
isLoading={isLoading}
onEndReached={loadMore}
onEndReachedThreshold={8}
pagination={false}
/>Column pinning
Pinning via column definition:
const columns: ColumnType<User>[] = [
{ key: 'name', dataIndex: 'name', title: 'Name', pinned: 'left', width: 200 },
{ key: 'email', dataIndex: 'email', title: 'Email', width: 250 },
{ key: 'actions', dataIndex: 'actions', title: 'Actions', pinned: 'right', width: 100 },
];Users can also pin/unpin columns at runtime via the right-click context menu.
Custom cell rendering
const columns: ColumnType<User>[] = [
{
key: 'status',
dataIndex: 'status',
title: 'Status',
width: 120,
render: (value, record) => (
<span
style={{
padding: '2px 8px',
borderRadius: 4,
fontSize: 12,
backgroundColor: record.status === 'active' ? '#d1fae5' : '#fee2e2',
color: record.status === 'active' ? '#065f46' : '#991b1b',
}}
>
{String(value)}
</span>
),
},
];Custom context menu items
<BoltTable
columns={columns}
data={data}
columnContextMenuItems={[
{
key: 'copy',
label: 'Copy column data',
icon: <CopyIcon className="h-3 w-3" />,
onClick: (columnKey) => copyColumnToClipboard(columnKey),
},
{
key: 'reset-width',
label: 'Reset width',
onClick: (columnKey) => resetColumnWidth(columnKey),
},
]}
/>Styling overrides
<BoltTable
columns={columns}
data={data}
accentColor="#6366f1"
classNames={{
header: 'text-xs uppercase tracking-wider text-gray-500',
cell: 'text-sm',
pinnedHeader: 'border-r border-indigo-200',
pinnedCell: 'border-r border-indigo-100',
}}
styles={{
header: { fontWeight: 600 },
rowHover: { backgroundColor: '#f0f9ff' },
rowSelected: { backgroundColor: '#e0e7ff' },
pinnedBg: 'rgba(238, 242, 255, 0.95)',
}}
/>Loading skeleton
// Full skeleton on initial load (no data yet)
<BoltTable
columns={columns}
data={[]}
isLoading={true}
pagination={{ pageSize: 20 }}
/>
// Layout skeleton before column widths are known
<BoltTable
columns={columns}
data={[]}
layoutLoading={true}
/>Fixed height (fill parent)
By default, BoltTable auto-sizes to its content. To fill a fixed-height container instead:
<div style={{ height: 600 }}>
<BoltTable
columns={columns}
data={data}
autoHeight={false}
/>
</div>Type exports
import type {
ColumnType,
ColumnContextMenuItem,
RowSelectionConfig,
ExpandableConfig,
PaginationType,
SortDirection,
DataRecord,
} from '@venkateshsirigineedi/bolt-table';License
MIT © Venkatesh Sirigineedi
