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

@polycyphers/spreadsheet

v1.0.18

Published

High-performance virtual-scrolled React spreadsheet with Excel I/O, formula engine, and plugin system

Readme

@polycyphers/spreadsheet

High-performance virtual-scrolled React spreadsheet with Excel I/O, a formula engine, and a plugin system.

Commercial. Production use requires a license — unlicensed instances render a visible watermark. A free 30-day trial is available, email-gated, no credit card. Details + pricing at packages.polycyphers.com/spreadsheet.

Highlights

  • Virtualized — handles 100k+ rows without dropping frames
  • Excel I/O — import/export XLSX with styles, formulas, merges
  • Formula engine — 100+ built-in functions, custom registrations, cross-sheet refs
  • Typed cells — text, number, currency, percentage, date, select, checkbox, formula
  • Sort / filter / pin / merge — all standard grid affordances
  • Clipboard + undo/redo — tab-delimited, batched history
  • Plugin systemCorePlugin<T> extension points for custom columns, validation, toolbars
  • React 19 + TypeScript-first, DaisyUI-compatible styles, tree-shakable

Install

npm install @polycyphers/spreadsheet

Setup

Import the stylesheet once in your app entry (e.g. main.tsx):

import "@polycyphers/spreadsheet/css";

Peer dependencies — your app must have:

  • react >= 18
  • react-dom >= 18
  • exceljs >= 4.4.0 (optional — required only for Excel I/O)

Styling requirements (not peer deps, but expected by the CSS):

  • Tailwind CSS + DaisyUI — the stylesheet references DaisyUI theme CSS variables

Quick Start

import { Spreadsheet, type SpreadsheetColumn } from "@polycyphers/spreadsheet";

interface Employee {
  id: number;
  name: string;
  department: string;
}

const columns: SpreadsheetColumn<Employee>[] = [
  { key: "id", label: "ID", width: 60 },
  { key: "name", label: "Name", width: 200 },
  { key: "department", label: "Department", width: 150 },
];

function MyPage() {
  const [data, setData] = useState<Employee[]>([]);

  return (
    <Spreadsheet
      data={data}
      columns={columns}
      persistKey="my-spreadsheet"
    />
  );
}

Features

| Feature | Description | |---------|-------------| | Virtual Scrolling | Renders only visible rows — handles 100k+ rows smoothly | | Column Resize | Drag-to-resize with localStorage persistence, batch resize for multi-selected columns | | Row Height Resize | Drag row handles or double-click auto-fit; batch resize selected rows | | Sorting | Click column headers, async transitions | | Filtering | Global search + per-column dropdown filters | | Cell Selection | Single cell, row, column, and range selection | | Inline Editing | Text, number, select, checkbox, date, and custom editors | | Excel Import (Full Fidelity) | Import .xlsx preserving fonts, colors, borders, alignment, number formats, merged cells, freeze panes, column widths, row heights, multi-sheet workbooks, and formulas | | Excel Export | Export to .xlsx with styling (via ExcelJS) | | CSV Export | RFC 4180-compliant CSV export | | Formula Engine | 70+ built-in functions, dependency graph, circular reference detection, live evaluation | | Formula Bar | View/edit active cell formula with commit button | | Multi-Sheet Tabs | Automatic sheet tabs from imported workbooks; configurable prop-based tabs | | Cell Merging | Merge cells across columns with O(1) lookup | | Freeze Panes | Freeze left columns and top rows (from props or auto-applied from Excel import) | | Copy / Paste | Ctrl+C/X/V with system clipboard | | Undo / Redo | 100-step history, Ctrl+Z / Ctrl+Shift+Z | | Status Bar | Sum, Avg, Count, Min, Max for multi-cell selections | | Plugin System | Extensible lifecycle hooks for custom behavior | | Context Menu | Right-click menus with visibility/disabled controls | | Session Persistence | Column widths, sort state saved to storage |

Excel Import — Full Fidelity

The spreadsheet can display an imported Excel (or Apple Numbers) file exactly as it appears in the original, including all formatting.

Basic import (matched columns)

const ref = useRef<SpreadsheetRef<MyRow>>(null);

// Trigger from a file input
const handleFile = (e: React.ChangeEvent<HTMLInputElement>) => {
  const file = e.target.files?.[0];
  if (file) ref.current?.importFromExcel(file);
};

Free-form import (auto-generates columns from Excel headers)

When you don't know the column structure ahead of time, use freeFormImport. The spreadsheet will auto-generate column definitions from the Excel header row and display the file exactly as-is.

<Spreadsheet
  data={data}
  columns={columns}
  excelConfig={{ freeFormImport: true }}
  ref={ref}
/>

What is extracted

| Excel property | Extracted | |---|---| | Cell values | ✅ | | Formulas (=SUM(A1:B5), etc.) | ✅ evaluated live by formula engine | | Font (bold, italic, underline, strikethrough, size, color, family) | ✅ | | Background fill color | ✅ | | Text alignment (horizontal + vertical) | ✅ | | Word wrap | ✅ | | Borders (top, bottom, left, right — style + color) | ✅ | | Number formats (%, currency, decimals) | ✅ displayed + applied | | Merged cells | ✅ | | Freeze panes | ✅ | | Column widths | ✅ | | Row heights | ✅ | | All sheets (multi-sheet workbook) | ✅ tabs rendered automatically | | Rich text | ✅ (text content extracted) | | Dates | ✅ |

Types for custom import processing

import type {
  CellStyle,
  CellStyleMap,
  ExcelImportMeta,
  ExcelSheetData,
} from "@polycyphers/spreadsheet";

Exports

Component

import { Spreadsheet } from "@polycyphers/spreadsheet";

Sub-Components

import { SheetTabs, LongTextDialog } from "@polycyphers/spreadsheet";

Hooks

import {
  useVirtualScroll,
  useColumnResize,
  useRowHeightResize,
  useSelection,
  useSorting,
  useFiltering,
  useExcelIO,
  useFormulaEngine,
  usePinning,
  useMergedCells,
  useStatusBar,
  useClipboard,
  useUndoRedo,
  useDynamicColumns,
  useOverflowDetection,
} from "@polycyphers/spreadsheet";

Plugin Utilities

import { usePlugins, createPlugin, composePlugins } from "@polycyphers/spreadsheet";

Feature Hooks

import {
  useHourValidation,
  useCostCodes,
  useInlineRowCreation,
  useContextMenu,
} from "@polycyphers/spreadsheet";

Types

import type {
  // Core
  SpreadsheetColumn,
  SpreadsheetProps,
  SpreadsheetRef,
  SpreadsheetMode,              // "view" | "edit" | "create"
  SpreadsheetVariant,           // "default" | "compact" | "pointage" | "tasks"
  SpreadsheetExcelConfig,
  SpreadsheetFeatures,
  CellEditorType,               // "text" | "number" | "select" | "checkbox" | "date" | "custom"

  // Selection & navigation
  CellPosition,
  SelectionRange,
  SelectionMode,                // "cell" | "row" | "column"
  NavigationDirection,          // "down" | "up" | "right" | "left"
  PersistedSheetState,

  // Sheet tabs
  SheetTab,

  // Excel import
  CellBorder,
  CellStyle,
  CellStyleMap,
  ExcelImportMeta,
  ExcelSheetData,

  // Feature configs
  InlineRowCreationConfig,
  RowHeightResizeConfig,
  ContextMenuConfig,
  ContextMenuItem,
  CostCodeDialogConfig,
  HourValidationConfig,
  HourValidationError,
  CalculatedFieldConfig,
  DirtyTrackingConfig,
  DynamicColumnConfig,
  MultiRowSelectionConfig,
  PositionSummaryConfig,

  // Plugin system
  SpreadsheetPlugin,
  PluginContext,
  CellInfo,
} from "@polycyphers/spreadsheet";

Hook option/result types (UseVirtualScrollOptions, VirtualScrollResult, UseColumnResizeOptions, ColumnResizeResult, UseSelectionOptions, SelectionResult, UseSortingOptions, SortingResult, UseFilteringOptions, FilteringResult, UseExcelIOOptions, ExcelIOResult, UseClipboardOptions, ClipboardResult, UseUndoRedoOptions, UndoRedoResult, CellChange, UndoRedoEntry, UsePinningOptions, PinningResult, PinningConfig, UseMergedCellsOptions, MergedCellsResult, MergedCellRange, MergedCellInfo, UseStatusBarOptions, StatusBarResult, StatusBarStats, UseFormulaEngineOptions, FormulaEngineResult, UseRowHeightResizeOptions, RowHeightResizeResult, UseDynamicColumnsOptions, DynamicColumnsResult, UseHourValidationOptions, UseHourValidationResult, UseCostCodesOptions, UseCostCodesResult, UseInlineRowCreationOptions, UseInlineRowCreationResult, UseContextMenuOptions, UseContextMenuResult, UsePluginsOptions, UsePluginsResult, PointagePluginConfig, TasksPluginConfig) are all exported for advanced composition.

Formula Engine

import {
  FormulaEngine,               // standalone class
  DependencyGraph,             // cell dependency graph
  Errors, FormulaError, isError,
  builtinFunctions, functionCatalog,
  colToNum, numToCol, cellId, parseCellId, adjustFormulaRefs,
} from "@polycyphers/spreadsheet";

import type {
  CellValue,
  CellMatrix,
  CellData,
  CellRef,
  FormulaFunction,
  FunctionInfo,
  ASTNode,
} from "@polycyphers/spreadsheet";

Editor sub-components

import {
  CellTextEditor,
  CellSelectEditor,
  OverflowCellContent,
} from "@polycyphers/spreadsheet";

import type {
  CellTextEditorProps,
  CellSelectEditorProps,
  OverflowCellContentProps,
} from "@polycyphers/spreadsheet";

Utilities

import {
  columnIndexToLetter,
  getValue,
  useOverflowDetection,
} from "@polycyphers/spreadsheet";

Constants

import {
  DEFAULT_ROW_HEIGHT,          // 32
  DEFAULT_OVERSCAN,            // 8
  DEFAULT_MIN_BUFFER_ROWS,     // 8
  DEFAULT_MIN_RENDER_ROWS,     // 30
  DEFAULT_COLUMN_MIN_WIDTH,    // 60
  DEFAULT_COLUMN_MAX_WIDTH,    // 600
} from "@polycyphers/spreadsheet";

License

import {
  setLicense,
  getLicenseStatus,
  isLicensed,
  KARAMENT_INTERNAL_KEY,
} from "@polycyphers/spreadsheet";

import type { LicenseStatus } from "@polycyphers/spreadsheet";

Props Reference

Core

| Prop | Type | Default | Description | |------|------|---------|-------------| | data | T[] | — | Row objects | | columns | SpreadsheetColumn<T>[] | — | Column definitions | | mode | "view" \| "edit" \| "create" | "view" | Interaction mode | | editContextKey | string \| number \| null | — | Changing this flushes pending edits (e.g. switching record) | | loading | boolean | false | Show loading state | | emptyMessage | string | — | Message shown when data is empty | | containerClassName | string | — | Extra class on the outer container | | persistKey | string | — | localStorage/sessionStorage key for column widths, sort, filters | | variant | "default" \| "compact" \| "pointage" \| "tasks" | "default" | Layout density variant |

Layout / Virtualization

| Prop | Type | Default | Description | |------|------|---------|-------------| | rowHeight | number \| (row,i)=>number | 32 | Fixed or per-row height | | maxHeight | number \| string | — | Scroll region max height | | overscan | number | 8 | Extra rows rendered outside viewport | | minBufferRows | number | 8 | Minimum buffered rows above/below viewport | | minRenderRows | number | 30 | Minimum rendered rows (prevents underdraw) | | pinning | {columns?,rows?} | — | Freeze left columns / top rows | | mergedCells | [{row,col,rowspan,colspan}] | — | Merged cell ranges |

Slots

| Prop | Type | Description | |------|------|-------------| | toolbar | ReactNode | Custom formula bar toolbar | | toolbarLeft | ReactNode | Content pinned to left of toolbar | | formulaBarStart | ReactNode | Content before formula bar | | hideFormulaBar | boolean | Hide the formula bar entirely | | footer | ReactNode | Footer content below the grid | | slotBelowHeader | ReactNode | Content injected below the column headers | | summaryRow | (rows, ctx?) => ReactNode | Totals/summary row renderer | | sheetTabs | SheetTab[] | Multi-sheet tabs | | activeSheetTab | string | Active sheet tab key | | onSheetTabChange | (key) => void | Tab change handler |

Callbacks

| Prop | Type | Description | |------|------|-------------| | onCellChange | (rowIndex, columnKey, value, row) => void | Single cell value changed | | onRowChange | (rowIndex, row) => void | Any cell on a row changed | | onDataChange | (rows) => void | Full dataset changed (paste, import, undo, etc.) | | onRowClick | (row, index) => void | Row clicked | | onRowDoubleClick | (row, index) => void | Row double-clicked | | onRowContextMenu | (row, index, event) => void | Row right-clicked | | onSelectionChange | (cell \| null, row?) => void | Active cell/row selection changed | | onSearchChange | (search) => void | Global search text changed |

Row customization

| Prop | Type | Description | |------|------|-------------| | rowClassName | (row, i) => string \| undefined | Extra class per row | | rowNumberRender | (row, i) => ReactNode | Custom row number cell | | rowNumberTitle | (row, i) => string \| undefined | Tooltip for row number | | isCellEditable | (row, column, i) => boolean | Per-cell edit guard (overrides column.editable) | | actionsRender | (row, i) => ReactNode | Right-hand actions column renderer | | actionsColumnWidth | number | Width of actions column | | actionsColumnResizable | boolean | Allow actions column to be resized | | getRowId | (row, i) => string \| number | Stable row identity (used by scrollToRowId) | | scrollToRowId | string \| number \| null | Imperatively scroll to a row by id |

Feature flags

| Prop | Type | Default | Description | |------|------|---------|-------------| | allowKeyboardNavigation | boolean | true | Arrow/Tab/Enter navigation | | allowColumnResize | boolean | true | Drag-to-resize column widths | | allowFilters | boolean | true | Column filter dropdowns | | allowSorting | boolean | true | Column header sort | | showStatusBar | boolean | false | Sum/Avg/Count/Min/Max bar |

Excel I/O

| Prop | Type | Description | |------|------|-------------| | excelConfig.fileName | string | Download filename for export | | excelConfig.sheetName | string | Sheet name for export | | excelConfig.freeFormImport | boolean | Auto-generate columns from Excel headers | | excelConfig.exportBuilder | fn | Custom workbook builder | | excelConfig.importParser | fn | Custom import parser (bypasses fidelity import) |

Extensibility

| Prop | Type | Description | |------|------|-------------| | plugins | SpreadsheetPlugin<T>[] | Lifecycle/event/render plugin array | | features | SpreadsheetFeatures<T> | Feature config bag: inlineRowCreation, rowHeightResize, contextMenu, multiRowSelection, costCodeDialog, hourValidation, positionSummary, calculatedFields, dirtyTracking, dynamicColumns | | aiId | string | AI integration id (for external assistants) |

SpreadsheetRef methods

const ref = useRef<SpreadsheetRef<T>>(null);

ref.current.exportToExcel()         // download .xlsx
ref.current.importFromExcel(file)   // load file with full fidelity
ref.current.exportToCSV()           // download .csv
ref.current.undo()
ref.current.redo()
ref.current.getData()               // get current rows
ref.current.resetColumnWidths()

Plugin System

Plugins implement the SpreadsheetPlugin<T> interface. Every hook is optional — implement only what you need. Return true from an event hook to prevent the default behavior.

Plugin lifecycle & hook surface

| Hook | Signature | Purpose | |------|-----------|---------| | onMount | (context) => void \| (() => void) | Mount hook; return function runs on unmount | | onUnmount | () => void | Explicit unmount callback | | onDataChange | (data, prevData) => void | Fires when the row array reference changes | | renderCellOverlay | (cell) => ReactNode | Overlay UI rendered above a cell | | renderRowOverlay | (row, i) => ReactNode | Overlay rendered per row | | renderToolbarExtension | (context) => ReactNode | Injects content into the formula bar toolbar | | renderFooterExtension | (context) => ReactNode | Injects content into the footer area | | onCellClick / onCellDoubleClick | (cell, event) => boolean \| void | Return true to prevent default | | onCellChange | (cell, newValue, prevValue) => any | Return a value to transform the committed value | | onRowClick | (row, i, event) => boolean \| void | Return true to prevent default | | onKeyDown | (event, cell) => boolean \| void | Return true to prevent default | | onContextMenu | (row, i, event) => boolean \| void | Return true to prevent default | | transformData | (data) => data | Last-mile data transform (sort/filter/inject synthetic rows) | | transformColumns | (columns) => columns | Last-mile column transform | | validateCell | (cell, newValue) => { valid, message? } | Veto a cell edit |

PluginContext<T> provides { data, columns, mode, selectedCell, selectionRange, updateCellValue, setSelectedCell, getRow, getColumn, forceUpdate }.

Building a plugin

import { createPlugin, type SpreadsheetPlugin } from "@polycyphers/spreadsheet";

const uppercasePlugin: SpreadsheetPlugin<MyRow> = createPlugin({
  name: "uppercase-names",
  onCellChange: (cell, newValue) => {
    if (cell.columnKey === "name" && typeof newValue === "string") {
      return newValue.toUpperCase();
    }
  },
});

<Spreadsheet data={rows} columns={cols} plugins={[uppercasePlugin]} />

Combine plugins with composePlugins(a, b, c).

Built-in plugins

| Plugin | Factory | Exported columns | Use case | |--------|---------|------------------|----------| | Pointage | createPointagePlugin(config) | POINTAGE_COLUMNS | Hour validation, bulk regular hours, position summary, salary type grouping | | Tasks | createTasksPlugin(config) | TASK_COLUMNS | Inline row creation, context menu, row height resize, status/category dropdowns |

Config types: PointagePluginConfig, TasksPluginConfig.

Variants

The variant prop adjusts layout density:

| Variant | Description | |---------|-------------| | "default" | Standard spreadsheet | | "compact" | Reduced row height and padding | | "pointage" | Optimized for attendance/hours entry | | "tasks" | Optimized for task management |

Formula Engine

Supports 70+ Excel-compatible functions. Introspect the full set at runtime via the exported functionCatalog.

Math: SUM, AVERAGE, COUNT, COUNTA, MIN, MAX, ROUND, ROUNDUP, ROUNDDOWN, CEILING, FLOOR, ABS, POWER, SQRT, MOD, INT, PRODUCT, PI, RAND, SIGN
Statistical: MEDIAN, MODE, STDEV, VAR, LARGE, SMALL, RANK, PERCENTILE
Logical: IF, IFS, AND, OR, NOT, IFERROR, SWITCH
Text: CONCATENATE, LEFT, RIGHT, MID, LEN, UPPER, LOWER, TRIM, SUBSTITUTE, FIND, SEARCH, TEXT, VALUE
Date: TODAY, NOW, DATE, YEAR, MONTH, DAY, DATEDIF, DAYS
Lookup: VLOOKUP, HLOOKUP, INDEX, MATCH
Financial: PMT, FV, PV, RATE
Information: ISBLANK, ISERROR, ISNUMBER, ISTEXT, SUMIF, COUNTIF, AVERAGEIF

Cell references support absolute ($A$1), relative (A1), and mixed forms.

Consumer integration

Required app setup

  1. Install the packagenpm install @polycyphers/spreadsheet.
  2. Install peer deps in the app — react >= 18, react-dom >= 18, and exceljs >= 4.4.0 if Excel I/O is used.
  3. Import the stylesheet once in the app entry:
    // main.tsx
    import "@polycyphers/spreadsheet/css";
  4. Tailwind + DaisyUI must be configured in the host app — the stylesheet consumes DaisyUI theme CSS variables (e.g. --b1, --bc, --p).

Vite alias — live source development

Use this pattern in a consumer's vite.config.ts to skip the build step while iterating on the package. Verified in PORTAL/Claims/vite.config.ts:

import path from "path";

const spreadsheetSourceRoot = path.resolve(
  __dirname,
  "../../karament-spreadsheet/src"
);

export default defineConfig({
  resolve: {
    dedupe: ["react", "react-dom", "@iconify/react"],
    alias: [
      { find: "@polycyphers/spreadsheet/css", replacement: path.join(spreadsheetSourceRoot, "spreadsheet.css") },
      { find: "@polycyphers/spreadsheet",      replacement: path.join(spreadsheetSourceRoot, "index.ts") },
    ],
  },
  optimizeDeps: {
    exclude: ["@polycyphers/spreadsheet"],
  },
});

Key points:

| Setting | Why | |---------|-----| | resolve.dedupe: ["react", "react-dom", "@iconify/react"] | Prevents duplicate React copies (needed for hooks to work when package is resolved from source) | | alias for @polycyphers/spreadsheet | Points import directly at src/index.ts — no rebuild to pick up edits | | Separate alias for /css | CSS lives outside the JS entry; must be aliased explicitly | | optimizeDeps.exclude | Keeps Vite from pre-bundling the package when using the alias |

Without the alias, consumers just use the published dist/ output — no special config required.

Adopter gotchas

  • Do not import @polycyphers/spreadsheet/css inside the package's own JS — imports happen once in the app entry, never transitively from the library. Breaking this splits the CSS out of the single-file bundle.
  • React version mismatch is the #1 failure mode when using the Vite alias (duplicate copies → Invalid hook call). Always add resolve.dedupe.
  • ExcelJS is optional but peerDependenciesMeta marks it so — the app gets a bundler warning if it's missing and Excel I/O is called. Install it if importFromExcel / exportToExcel are used.
  • persistKey writes column widths, sort state, and filters to storage. Use a unique key per page; reusing a key across pages cross-contaminates state.
  • editContextKey must be bumped when the underlying record changes (e.g. navigating from record A to record B with the same page mounted). Without it, pending edits from A will flush into B.
  • Merged cells and pinning from Excel import override any mergedCells / pinning props on the component — imported files are treated as authoritative.
  • CSS is one file (spreadsheet.css, not code-split). Size is predictable; import it only once.
  • Icons — the package ships its own @iconify/react + @iconify/icons-lucide runtime dependencies. If the app also uses Iconify, add it to resolve.dedupe (see above) to avoid two registries.
  • Free-form Excel import re-generates columns from the file header row — any column props you pass in are discarded when excelConfig.freeFormImport: true.

Updating

When a new version is tagged:

npm install @polycyphers/spreadsheet

Development

git clone https://github.com/melejel2/karament-spreadsheet.git
cd karament-spreadsheet
npm install
npm run build        # Build dist/
npm run dev          # Watch mode
npm run type-check   # TypeScript validation

License

Commercial. See LICENSE.md and EULA.md. Trial + pricing at packages.polycyphers.com/spreadsheet.


Polycyphers © 2025-2026 | All rights reserved