npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

bolt-table

v0.1.23

Published

Virtualized React table with column drag & drop, pinning, resizing, sorting, filtering, and pagination.

Readme

bolt-table

A high-performance, zero-dependency* React table component. Only the rows visible in the viewport are ever in the DOM — making it fast for datasets of any size uisng TanStack Virtual.

npm version license github website


Features

  • Row virtualization — only visible rows are rendered, powered by TanStack Virtual
  • Drag to reorder columns — custom zero-dependency drag-and-drop (no @dnd-kit needed)
  • 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 scrollonEndReached callback 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
  • Row pinning — pin rows to the top or bottom of the table, sticky during vertical scroll
  • Cell context menu — right-click (or long-press on mobile) any cell to pin rows or copy values
  • Right-click context menu — sort, filter, pin, hide, plus custom items
  • Mobile-friendly context menus — long-press (touch-and-hold) triggers context menus on touch devices
  • Theme-agnostic — works in light and dark mode out of the box, no CSS variables needed
  • Custom icons — override any built-in icon via the icons prop

Installation

npm install bolt-table @tanstack/react-virtual

That's it. No other peer dependencies.


Quick start

import { BoltTable, ColumnType } from '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:

'use client';
import { BoltTable } from 'bolt-table';

Styling

BoltTable uses inline CSS styles for all defaults — no Tailwind, no CSS variables, no external stylesheets required. It works out of the box in any React project, light or dark mode.

You can customize everything via the styles and classNames props. If your project uses Tailwind, you can pass Tailwind classes through classNames and they'll be applied on top of the inline defaults.

Custom icons

All built-in icons are inline SVGs. Override any icon via the icons prop:

import type { BoltTableIcons } from 'bolt-table';

<BoltTable
  icons={{
    gripVertical: <MyGripIcon size={12} />,
    sortAsc: <MySortUpIcon size={12} />,
    chevronsLeft: <MyFirstPageIcon size={12} />,
  }}
/>

Available icon keys: gripVertical, sortAsc, sortDesc, filter, filterClear, pin, pinOff, eyeOff, chevronDown, chevronLeft, chevronRight, chevronsLeft, chevronsRight, copy.


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 | | icons | BoltTableIcons | — | Custom icon overrides for built-in SVG icons | | gripIcon | ReactNode | — | Custom drag grip icon (deprecated, use icons.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 | | rowPinning | RowPinningConfig | — | Row pinning config ({ top?: Key[], bottom?: Key[] }) | | onRowPin | (rowKey, pinned) => void | — | Called when a row is pinned/unpinned via cell context menu | | 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 | | copy | boolean \| (value, record, index) => string | — | Enable "Copy" in cell context menu; function customizes what's copied |


Examples

Sorting

Client-side (no onSortChange — BoltTable sorts locally):

const columns: ColumnType<User>[] = [
  {
    key: 'name',
    dataIndex: 'name',
    title: 'Name',
    sortable: true,
    sorter: (a, b) => a.name.localeCompare(b.name),
  },
  {
    key: 'age',
    dataIndex: 'age',
    title: 'Age',
    sortable: true,
  },
];

<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,
    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}
  pagination={{ pageSize: 20 }}
  onPaginationChange={(page, size) => setPage(page)}
/>

Server-side (pass only the current page):

<BoltTable
  columns={columns}
  data={currentPageData}
  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',
    selectedRowKeys,
    onChange: (keys, rows) => setSelectedRowKeys(keys),
    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>
    ),
  }}
  expandedRowHeight={150}
  maxExpandedRowHeight={400}
/>

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

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.


Row pinning

Pin rows to the top or bottom of the table so they stay visible while scrolling vertically. Pinned rows transcend pagination — they are always visible regardless of which page the user is on.

const [rowPinning, setRowPinning] = useState({ top: [], bottom: [] });

<BoltTable
  columns={columns}
  data={data}
  rowKey="id"
  rowPinning={rowPinning}
  onRowPin={(key, pinned) => {
    setRowPinning(prev => {
      const top = (prev.top ?? []).filter(k => String(k) !== String(key));
      const bottom = (prev.bottom ?? []).filter(k => String(k) !== String(key));
      if (pinned === 'top') top.push(key);
      if (pinned === 'bottom') bottom.push(key);
      return { top, bottom };
    });
  }}
  styles={{ pinnedRowBg: 'rgba(255, 255, 255, 0.95)' }}
/>

Users can also pin/unpin rows at runtime via the right-click context menu on any body cell (when onRowPin is provided).

Pinned rows use position: sticky with backdropFilter: blur(12px) and a subtle box-shadow to visually separate them from scrolling content. Customize with classNames.pinnedRow, styles.pinnedRow, and styles.pinnedRowBg.


Cell context menu & copy

Right-click (or long-press on mobile) any body cell to see a context menu with:

  • Pin to Top / Unpin from Top — shown when onRowPin is provided
  • Pin to Bottom / Unpin from Bottom — shown when onRowPin is provided
  • Copy — shown when the column has copy: true or a copy function
const columns: ColumnType<User>[] = [
  {
    key: 'name',
    dataIndex: 'name',
    title: 'Name',
    copy: true, // copies the raw cell value
  },
  {
    key: 'email',
    dataIndex: 'email',
    title: 'Email',
    // Custom copy — control exactly what goes to the clipboard
    copy: (value, record) => `${record.name} <${value}>`,
  },
];

The cell context menu only appears when there is at least one action available (either onRowPin or column.copy). Otherwise, the browser's default context menu is used.


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)',
  }}
/>

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>

Documentation

For the complete guide with in-depth examples for every feature, visit the BoltTable Documentation.


Type exports

import type {
  ColumnType,
  ColumnContextMenuItem,
  RowSelectionConfig,
  RowPinningConfig,
  ExpandableConfig,
  PaginationType,
  SortDirection,
  DataRecord,
  BoltTableIcons,
} from 'bolt-table';

License

MIT © Venkatesh Sirigineedi