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

@ricsam/react-spreadsheets

v0.0.6

Published

Headless-friendly React spreadsheet and workbook components: infinite virtualized grid, zoom, resizing, clipboard, fill handles and formula-engine bindings.

Readme

@ricsam/react-spreadsheets

React spreadsheet primitives: an infinitely scrollable, virtualized grid with selection, inline editing, clipboard, fill handles, column/row resizing and floating overlays — plus optional bindings for @ricsam/formula-engine.

  • Infinite grid — rows and columns are unbounded; only visible cells render.
  • Canvas gridlines + DOM cells — crisp lines, fully stylable cells.
  • Zero styling dependencies — one plain CSS file themed with custom properties.
  • Bring your own state — controlled or uncontrolled cell data.
  • Optional formula engine — drop in FormulaSheet / FormulaWorkbook for formulas, spill ranges, tables, styles and multi-sheet workbooks.

Installation

bun add @ricsam/react-spreadsheets @ricsam/selection-manager
# optional, for the formula-aware components
bun add @ricsam/formula-engine

react, react-dom and @ricsam/selection-manager are peer dependencies.

Usage

Import the stylesheet once, near your app root:

import "@ricsam/react-spreadsheets/styles.css";

Standalone grid

import { useState } from "react";
import { Spreadsheet, type SerializedCellValue } from "@ricsam/react-spreadsheets";
import "@ricsam/react-spreadsheets/styles.css";

export function Demo() {
  const [cells, setCells] = useState<Map<string, SerializedCellValue>>(
    () => new Map([["A1", "Region"], ["B1", "Revenue"], ["A2", "EMEA"], ["B2", 120]]),
  );

  return (
    <Spreadsheet
      style={{ height: 480 }}
      cellData={cells}
      onCellDataChange={setCells}
    />
  );
}

With the formula engine

import { FormulaEngine } from "@ricsam/formula-engine";
import { FormulaSheet } from "@ricsam/react-spreadsheets";
import "@ricsam/react-spreadsheets/styles.css";

const engine = FormulaEngine.buildEmpty();
engine.addWorkbook("Workbook1");
engine.addSheet({ workbookName: "Workbook1", sheetName: "Sheet1" });
engine.setSheetContent(
  { workbookName: "Workbook1", sheetName: "Sheet1" },
  new Map([
    ["A1", 10],
    ["A2", 20],
    ["A3", "=SUM(A1:A2)"], // renders 30
  ]),
);

export function Demo() {
  return (
    <FormulaSheet
      engine={engine}
      workbookName="Workbook1"
      sheetName="Sheet1"
      style={{ height: 480 }}
    />
  );
}

FormulaWorkbook adds Excel-style sheet tabs, renaming, deletion and zoom on top of FormulaSheet.

Theming

All colors, fonts and sizes are CSS custom properties. The package only ever reads the public --rsp-* names, so an override on any ancestor of the grid always wins:

.my-panel {
  --rsp-accent: #7c3aed;
  --rsp-bg: #ffffff;
  --rsp-header-bg: #f4f4f5;
  --rsp-cell-font-size: 13px;
}

An override replaces both schemes, so pick a value that works in the mode(s) you support, or scope it per scheme:

.my-panel { --rsp-bg: #ffffff; }
@media (prefers-color-scheme: dark) {
  .my-panel { --rsp-bg: #10131a; }
}

Light and dark

By default the grid follows the user's OS preference.

The grid deliberately does not declare color-scheme on .rsp-root, so a pin you set on an ancestor is respected. Pin a subtree with plain CSS:

.workbook-stage { color-scheme: light; } /* always light */
.workbook-stage { color-scheme: dark; }  /* always dark  */

or with the bundled helper classes:

<FormulaWorkbook className="rsp-theme-dark" ... />

Canvas gridlines and every DOM surface follow the same resolved scheme. The component resolves the inherited color-scheme and reflects it on the grid root as data-rsp-scheme, which drives the --rsp-light / --rsp-dark token switches.

Note — the stylesheet intentionally avoids light-dark(). Bundlers that target browsers without native support (Vite's Lightning CSS, for example) lower it to switch variables that only exist next to a color-scheme declaration, which makes shared-root tokens compute to an invalid value — in production builds only. An equivalent space-toggle is used instead.

Note — internally each public token is aliased to a private --_rsp-* property declared on .rsp-root, and the rules consume the private name. A custom property containing var() is substituted at the element where it is declared, so themed values declared on :root would be resolved against the document root and a subtree pin could never change them. Treat --_rsp-* as private and always override the public --rsp-* name.

Cell fills stay readable

A backgroundColor coming from your document model is usually a single literal color with no dark-mode variant. To stop light fills from pairing with the dark theme's near-white text, the grid derives a readable ink for any cell that sets a background but no explicit color. Set color yourself to opt out, and use getContrastingTextColor if you want the same behaviour elsewhere.

Per-cell styling is done in JS:

<Spreadsheet
  cellData={cells}
  customCellStyle={(cell) =>
    typeof cell.value === "number" && cell.value < 0
      ? { color: "#dc2626", fontWeight: 600 }
      : {}
  }
  customCellRenderer={(cell) => <span>{String(cell.value ?? "")}</span>}
/>

Key props

| Prop | Description | | --- | --- | | cellData | Map<string, SerializedCellValue> keyed by A1 reference. Omit for uncontrolled mode. | | onCellDataChange | Called with the next map after an edit, paste or fill. | | columnWidths / rowHeights | Controlled sizing, keyed by column letter / 1-based row. | | customCellStyle | Per-cell CSSProperties. | | customCellRenderer | Per-cell React node. | | parseValue | Coerce raw editor strings (e.g. text → number) before storing. | | selection | Selection state, callbacks and effects(selectionManager) for copy/paste/fill hooks. | | components + overlayChildren | Floating overlays anchored to the grid, optionally snapped to cell edges. |

Development

bun install
bun test        # unit + React rendering tests
bun run typecheck
bun run build   # emits dist/{mjs,cjs,types} + styles.css

License

MIT