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

wandertable

v0.1.0

Published

High-performance virtualized grid/spreadsheet library

Downloads

8

Readme

WanderTable

CI npm License: MIT Ko-fi Buy Me a Coffee

High-performance grid/spreadsheet library for the web. Zero dependencies, TypeScript, DOM-virtualized.

Features

  • Two modes: Spreadsheet (independent cells) and Table (column-driven rows)
  • Two layouts: scroll (fixed height, internal scroll, virtualized) and auto (grows with content, page scroll)
  • DOM virtualized: Only visible cells exist in the DOM (~750 nodes max). Recycled on scroll.
  • Rich cell content: Each cell is a real <div> - any HTML, CSS, hover effects, transitions
  • Renderers: text, number, checkbox, badge, progress, blank - or register your own
  • Editors: text, dropdown (async, searchable, multi-select), blank - or register your own
  • Cell-level types: Each cell can have its own renderer/editor independent of the column
  • Selection: Single, range (Shift), multi-range (Ctrl), full row/column, select all
  • Keyboard navigation: Arrows, Tab, Enter, Escape, Home/End, Ctrl+arrows, typing to edit
  • Clipboard: Copy/paste/cut as TSV (compatible with Excel/Sheets). Single-cell paste fills selection.
  • Undo/Redo: Command pattern with configurable stack limit
  • Sorting: Per-column, asc/desc, custom comparator, external callback for server-side
  • Filtering: Per-column, custom filter functions, external callback for server-side
  • Merge cells: Merge/unmerge arbitrary ranges
  • Frozen panes: Sticky rows and columns that stay visible on scroll
  • Grouped headers: Multi-level column headers (e.g. "Months / Jan Feb Mar")
  • Pagination: Built-in pagination bar with page size, total rows, navigation
  • Context menu: Dynamic items with visible/disabled as callbacks, per-target (cell/column/row header)
  • Resize: Column and row resize by dragging header edges (individually toggleable)
  • Auto-fit: Columns and rows auto-size to content
  • Auto row height: Rows automatically grow to fit cell content
  • Export: CSV and JSON with download, configurable columns/headers
  • Themes: Light, Dark, Minimal presets - or define your own via CSS variables
  • Loading overlay: Show/hide spinner overlay for async operations
  • Events: Typed event system for cell changes, selection, scroll, commands, editor lifecycle
  • Command events: command:execute, command:undo, command:redo for syncing with external databases
  • Fill handle: Drag to fill cells in any direction (including diagonal), with custom callback
  • Column reorder: Drag headers to reorder columns
  • Row grouping: Expand/collapse groups of rows
  • Infinite mode: Grid auto-expands on navigate/scroll beyond bounds
  • Lazy loading: onLoadMore callback when scroll reaches the edge
  • Sparse data model: Map-based, only cells with data consume memory. Patches for partial updates.

Install

npm install wandertable

Quick Start

Spreadsheet Mode

import { WanderTable } from 'wandertable';
import 'wandertable/style.css';

const table = new WanderTable(document.getElementById('grid'), {
  mode: 'spreadsheet',
  rowCount: 1000,
  colCount: 26,
  showRowHeaders: true,
  showColHeaders: true,
});

// Set individual cells
table.setCellValue(0, 0, 'Hello');
table.setCellValue(0, 1, 42);

// Bulk load
table.applyPatch([
  { row: 1, col: 0, data: { value: 'World', style: { fontWeight: 'bold' } } },
  { row: 1, col: 1, data: { value: true, type: 'checkbox' } },
  { row: 2, col: 0, data: { value: 75, type: 'progress' } },
  { row: 2, col: 1, data: { value: 'active', type: 'badge' } },
]);

Table Mode

const table = new WanderTable(document.getElementById('grid'), {
  mode: 'table',
  layout: 'auto', // grows with content, scroll on page
  autoRowHeight: true,
  columns: [
    { key: 'name', label: 'Name', width: 180 },
    { key: 'email', label: 'Email', width: 200 },
    { key: 'status', label: 'Status', type: 'badge', width: 100 },
    { key: 'active', label: 'Active', type: 'checkbox', width: 70 },
  ],
  tableData: [
    { name: 'Alice', email: '[email protected]', status: 'active', active: true },
    { name: 'Bob', email: '[email protected]', status: 'pending', active: false },
  ],
});

Grouped Column Headers

const table = new WanderTable(el, {
  mode: 'table',
  columns: [
    { key: 'name', label: 'Name', width: 180 },
    {
      label: 'Q4 Sales',
      children: [
        { key: 'oct', label: 'Oct', type: 'number', width: 80 },
        { key: 'nov', label: 'Nov', type: 'number', width: 80 },
        { key: 'dec', label: 'Dec', type: 'number', width: 80 },
      ],
    },
    {
      label: 'Metrics',
      children: [
        { key: 'score', label: 'Score', type: 'progress', width: 120 },
        { key: 'active', label: 'Active', type: 'checkbox', width: 70 },
      ],
    },
  ],
  tableData: [...],
});

Options

new WanderTable(container, {
  // ── Mode & Layout ──
  mode: 'spreadsheet' | 'table',       // default: 'spreadsheet'
  layout: 'scroll' | 'auto',           // default: 'scroll'

  // ── Data ──
  data: CellPatch[],                    // initial cell patches (spreadsheet mode)
  tableData: Record<string, unknown>[], // initial row objects (table mode)
  columns: ColumnConfig[],              // column definitions
  rowCount: number,                     // default: 100
  colCount: number,                     // default: 26

  // ── Dimensions ──
  defaultRowHeight: number,             // default: 28
  defaultColWidth: number,              // default: 100
  autoRowHeight: boolean,               // auto-fit row height to content. default: false

  // ── Display ──
  showRowHeaders: boolean,              // show row numbers. default: true
  showColHeaders: boolean,              // show column letters/labels. default: true
  showGridLines: boolean,               // show cell borders. default: true
  showBorder: boolean,                  // show outer border. default: true
  loading: boolean,                     // show loading overlay. default: false

  // ── Frozen Panes ──
  frozenRows: number,                   // sticky rows at top. default: 0
  frozenCols: number,                   // sticky columns at left. default: 0

  // ── Permissions ──
  readOnly: boolean,                    // no editing at all. default: false
  allowEditing: boolean,                // allow cell editing. default: true
  allowSelection: boolean,              // allow cell selection. default: true
  allowRangeSelection: boolean,         // allow multi-cell selection. default: true
  allowColResize: boolean,              // column resize by drag. default: true
  allowRowResize: boolean,              // row resize by drag. default: true
  allowClipboard: boolean,              // copy/paste/cut. default: true
  allowContextMenu: boolean,            // right-click menu. default: true
  allowKeyboardNav: boolean,            // keyboard navigation. default: true
  allowUndoRedo: boolean,               // undo/redo. default: true

  // ── Cell Defaults ──
  defaultCellType: string,              // renderer for cells without type. default: 'text'
  undoLimit: number,                    // max undo stack size. default: 100

  // ── Merge & Sort & Filter ──
  merges: MergeRange[],                 // initial merged ranges
  sort: { col, direction },             // initial sort
  filters: Record<number, FilterFn>,    // initial filters
  pagination: PaginationConfig,         // pagination settings

  // ── Theme ──
  theme: Partial<ThemeConfig>,          // or use THEME_DARK, THEME_MINIMAL

  // ── Callbacks ──
  onCellChange: (row, col, oldValue, newValue) => void,
  onSelectionChange: (ranges) => void,
  onScroll: (viewport) => void,
  onContextMenu: (row, col, event) => void,
  onSort: (col, direction, key?) => boolean | void,    // return true for external sort
  onFilter: (col, key, active) => boolean | void,      // return true for external filter
  onPageChange: (page, pageSize) => void,
});

API

Data

table.setCell(row, col, cellData)      // set full cell data
table.getCell(row, col)                // get cell data
table.setCellValue(row, col, value)    // set just the value (with undo)
table.deleteCell(row, col)             // remove cell
table.applyPatch(patches)             // bulk update (with undo)
table.transaction(() => { ... })       // batch changes, single re-render
table.setTableData(rows)              // replace all data (table mode)

Selection

table.getSelection()                   // get selected ranges
table.getSelectionInfo()               // rich info: focus, bounds, cellCount, isFullRow, etc.
table.setSelection(ranges)             // set selection programmatically
table.scrollTo(row, col)               // scroll to make cell visible

Undo / Redo

table.undo()
table.redo()
table.commands.canUndo                 // boolean
table.commands.canRedo                 // boolean
table.commands.clear()                 // clear history

Sort & Filter

table.sort(col, 'asc' | 'desc' | null, compareFn?)
table.clearSort()
table.getSortState()

table.setFilter(col, (row, data) => boolean)
table.removeFilter(col)
table.clearFilters()
table.getActiveFilters()               // column indices with active filters

Merge

table.mergeCells(row, col, rowSpan, colSpan)
table.unmergeCells(row, col)
table.getMerge(row, col)               // get merge at cell
table.clearMerges()

Layout

table.setColumnWidth(col, width)
table.setRowHeight(row, height)
table.autoFitColumn(col)               // auto-size to content
table.autoFitRow(row)                  // auto-size to content
table.insertRow(at)
table.insertColumn(at)
table.refresh()                        // force re-render

Export

table.exportCSV(options?)              // returns CSV string
table.exportJSON(options?)             // returns object[]
table.downloadCSV('file.csv', options?)
table.downloadJSON('file.json', options?)

Options: { visibleOnly, columns, includeHeaders, headers }

Pagination

table.setPage(page)                    // 0-based
table.setPageSize(size)
table.getPaginationState()             // { page, pageSize, totalRows, totalPages, startRow, endRow }
table.pagination.nextPage()
table.pagination.prevPage()
table.pagination.firstPage()
table.pagination.lastPage()

Loading

table.setLoading(true)                 // show spinner overlay
table.setLoading(false)                // hide
table.isLoading                        // boolean

Renderers & Editors

// Register custom renderer
table.registerRenderer('my-type', {
  render(cell: HTMLElement, data: CellData | undefined, state: CellState) {
    cell.innerHTML = `<div class="custom">${data?.value ?? ''}</div>`;
  }
});

// Register custom editor
table.registerEditor('my-editor', () => ({
  createElement(container) { ... },
  open(value, bounds, cellData) { ... },
  getValue() { ... },
  close() { ... },
}));

// Use per cell
table.setCell(0, 0, { value: 'hello', type: 'my-type' });

// Or per column
columns: [{ key: 'status', type: 'my-type', editor: 'my-editor' }]

Built-in renderers: text, number, checkbox, badge, progress, blank

Context Menu

// Override menu items
table.contextMenuItems = [
  {
    label: 'My Action',
    shortcut: 'Ctrl+M',
    visible: (ctx) => ctx.target === 'cell',        // show only for cells
    disabled: (ctx) => ctx.readOnly,                 // disable when readOnly
    action: (ctx) => console.log(ctx.row, ctx.col),
  },
  { label: '', separator: true },
  {
    label: 'Column Action',
    visible: (ctx) => ctx.target === 'column-header',
    action: (ctx) => console.log('col', ctx.col),
  },
];

MenuContext fields: target, row, col, selectedCount, isMultiSelect, isFullRow, isFullCol, readOnly, canUndo, canRedo

Events

table.on('cell:change', ({ row, col, oldValue, newValue }) => { ... })
table.on('cell:click', ({ row, col, event }) => { ... })
table.on('cell:dblclick', ({ row, col, event }) => { ... })
table.on('cell:contextmenu', ({ row, col, event }) => { ... })
table.on('selection:change', ({ ranges }) => { ... })
table.on('editor:open', ({ row, col }) => { ... })
table.on('editor:close', ({ row, col, value, cancelled }) => { ... })
table.on('scroll', ({ viewport }) => { ... })
table.on('column:resize', ({ col, width }) => { ... })
table.on('row:resize', ({ row, height }) => { ... })
table.on('data:patch', ({ patches }) => { ... })
table.on('command:execute', ({ changes }) => { ... })  // sync with DB
table.on('command:undo', ({ changes }) => { ... })
table.on('command:redo', ({ changes }) => { ... })

// Unsubscribe
const unsub = table.on('cell:change', handler);
unsub();

Themes

import { THEME_LIGHT, THEME_DARK, THEME_MINIMAL } from 'wandertable';

new WanderTable(el, { theme: THEME_DARK });

Or define your own via CSS variables:

.wt-root {
  --wt-bg: #0f172a;
  --wt-cell-bg: #1e293b;
  --wt-cell-text: #e2e8f0;
  --wt-header-bg: #0f172a;
  --wt-header-text: #94a3b8;
  --wt-grid-line: #334155;
  --wt-sel-bg: rgba(59, 130, 246, 0.15);
  --wt-sel-border: #3b82f6;
  --wt-focus-border: #60a5fa;
  --wt-font: 'Inter', sans-serif;
  --wt-font-size: 13px;
  --wt-header-font-size: 12px;
  --wt-cell-padding: 8px;
  --wt-header-height: 28px;
  --wt-row-header-width: 50px;
}

Fill Handle

Drag the small square at the bottom-right corner of a selection to fill cells.

new WanderTable(el, {
  allowFillHandle: true, // default: true

  // Optional: override default fill behavior
  onFill: (source, fill, direction) => {
    // direction: 'down' | 'up' | 'right' | 'left' | 'diagonal'

    // Return CellPatch[] for custom values
    // Return true to cancel fill
    // Return void to use default (repeat pattern)
  },
});

Column Reorder

new WanderTable(el, {
  allowColReorder: true, // drag column headers to reorder
});

table.reorderColumn(fromCol, toCol); // programmatic

Row Grouping

new WanderTable(el, {
  rowGroups: [
    { startRow: 0, count: 5, label: 'Group 1', expanded: true },
    { startRow: 6, count: 3, label: 'Group 2', expanded: false },
  ],
});

table.addRowGroup(startRow, count, label);
table.toggleRowGroup(startRow);
table.expandAllGroups();
table.collapseAllGroups();

Lazy Loading

new WanderTable(el, {
  onLoadMore: (direction) => {
    // direction: 'down' | 'right'
    fetchMoreData().then(rows => {
      table.applyPatch(rows);
    });
  },
});

Infinite Mode

new WanderTable(el, {
  infinite: true,  // grid expands as you navigate/scroll beyond bounds
  rowCount: 50,    // initial size
  colCount: 26,
});

Utilities

import { columnLabel } from 'wandertable';

columnLabel(0)   // 'A'
columnLabel(25)  // 'Z'
columnLabel(26)  // 'AA'
columnLabel(701) // 'ZZ'
columnLabel(702) // 'AAA'

Build Output

| File | Size | Description | |------|------|-------------| | dist/wandertable.js | ~112 KB | ESM module | | dist/wandertable.cjs | ~85 KB | CommonJS module | | dist/wandertable.umd.js | ~85 KB | UMD module | | dist/wandertable.global.js | ~85 KB | IIFE standalone (window.WanderTable) | | dist/style.css | ~8 KB | Styles | | dist/types/ | | TypeScript declarations |

Development

npm install
npm run dev          # dev server at http://localhost:5173
npm run build        # build library (ESM + CJS + types + CSS)
npm run typecheck    # type check
npm run test         # run 47 unit tests
npm run test:watch   # tests in watch mode

CI/CD

  • CI (.github/workflows/ci.yml): Runs on push/PR to main. Typecheck + tests + build on Node 20 and 22.
  • Publish (.github/workflows/publish.yml): Publishes to npm on GitHub release. Requires NPM_TOKEN secret.

License

MIT