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 🙏

© 2025 – Pkg Stats / Ryan Hefner

cellify

v0.3.0

Published

A lightweight, affordable spreadsheet library for building Excel-like experiences

Readme


*Only dependency is fflate for ZIP compression

Features

  • Excel Import/Export - Full .xlsx support with styles, formulas, merged cells
  • CSV Import/Export - Auto-detection of delimiters and data types
  • WASM-Accelerated - Optional Rust/WebAssembly parser for 10-50x faster imports
  • Complete Styling - Fonts, fills, borders, alignment, number formats
  • Cell Merging - With overlap detection and validation
  • Formulas - Store and preserve formulas (evaluation coming soon)
  • Cell Comments - Add comments with author information
  • Hyperlinks - URLs, email links, and internal cell references
  • Freeze Panes - Lock rows/columns for scrolling
  • Auto Filters - Enable filtering on data ranges
  • Type-Safe - Built with TypeScript from the ground up
  • Universal - Works in Node.js and browsers
  • Accessible - Built-in a11y helpers for screen readers

Demo

Try the interactive demo to test import/export functionality:

npm run demo

The demo includes:

  • Cell editing - Double-click or press Enter to edit cells
  • Right-click context menu - Copy, cut, paste, formatting, comments
  • Keyboard shortcuts - Ctrl+C/X/V, Ctrl+B/I, Ctrl+Z, arrow keys
  • Column/row selection - Click headers to select entire columns or rows
  • Column filters - Filter data by clicking the filter button in headers
  • Cell comments - Add, edit, and delete comments via context menu

Cellify Demo

Installation

npm install cellify

Quick Start

import { Workbook, workbookToXlsx } from 'cellify';

// Create a workbook
const workbook = new Workbook();
const sheet = workbook.addSheet('Sales Data');

// Set values
sheet.cell('A1').value = 'Product';
sheet.cell('B1').value = 'Revenue';
sheet.cell('A2').value = 'Widget';
sheet.cell('B2').value = 1500;

// Apply styles
sheet.cell('A1').style = {
  font: { bold: true, color: '#FFFFFF' },
  fill: { type: 'pattern', pattern: 'solid', foregroundColor: '#4472C4' },
};

// Export to Excel
const xlsxBuffer = workbookToXlsx(workbook);
// Save or send xlsxBuffer as .xlsx file

Excel (.xlsx) Export

Create Excel files with full styling support:

import { Workbook, workbookToXlsx, workbookToXlsxBlob } from 'cellify';

const workbook = new Workbook();
workbook.title = 'Sales Report';
workbook.author = 'Cellify';

const sheet = workbook.addSheet('Q4 Sales');

// Headers with styling
const headers = ['Product', 'Units', 'Price', 'Total'];
headers.forEach((header, i) => {
  const cell = sheet.cell(0, i);
  cell.value = header;
  cell.style = {
    font: { bold: true, size: 12, color: '#FFFFFF' },
    fill: { type: 'pattern', pattern: 'solid', foregroundColor: '#2F5496' },
    alignment: { horizontal: 'center' },
    borders: {
      bottom: { style: 'medium', color: '#000000' },
    },
  };
});

// Data rows
const data = [
  ['Laptop', 150, 999.99],
  ['Mouse', 500, 29.99],
  ['Keyboard', 300, 79.99],
];

data.forEach((row, rowIndex) => {
  row.forEach((value, colIndex) => {
    sheet.cell(rowIndex + 1, colIndex).value = value;
  });
  // Formula for total
  sheet.cell(rowIndex + 1, 3).setFormula(`B${rowIndex + 2}*C${rowIndex + 2}`);
});

// Set column widths
sheet.setColumnWidth(0, 15); // Product
sheet.setColumnWidth(1, 10); // Units
sheet.setColumnWidth(2, 12); // Price
sheet.setColumnWidth(3, 12); // Total

// Freeze header row
sheet.freeze(1, 0);

// Enable auto filter
sheet.setAutoFilter('A1:D4');

// Export
const buffer = workbookToXlsx(workbook); // Uint8Array for Node.js
const blob = workbookToXlsxBlob(workbook); // Blob for browsers

Excel (.xlsx) Import

Read existing Excel files:

import { xlsxToWorkbook, xlsxBlobToWorkbook } from 'cellify';

// From Uint8Array (Node.js)
const buffer = fs.readFileSync('report.xlsx');
const { workbook, stats, warnings } = xlsxToWorkbook(new Uint8Array(buffer));

console.log(`Imported ${stats.sheetCount} sheets, ${stats.totalCells} cells`);

// Access data
const sheet = workbook.getSheetByIndex(0);
console.log(sheet.cell('A1').value); // Read cell value
console.log(sheet.cell('B2').formula); // Read formula

// From Blob (browsers)
const file = document.getElementById('fileInput').files[0];
const result = await xlsxBlobToWorkbook(file);

// Import options
const { workbook } = xlsxToWorkbook(buffer, {
  sheets: ['Sheet1', 'Summary'], // Import specific sheets
  importFormulas: true,          // Preserve formulas
  importStyles: true,            // Import cell styles
  importMergedCells: true,       // Import merged cells
  importFreezePanes: true,       // Import freeze panes
  maxRows: 1000,                 // Limit rows (0 = unlimited)
  maxCols: 50,                   // Limit columns (0 = unlimited)
  onProgress: (phase, current, total) => {
    console.log(`${phase}: ${current}/${total}`);
  },
});

Performance: WASM Acceleration

Cellify includes an optional WebAssembly parser built with Rust that provides 10-50x faster XLSX imports for large files.

import { initXlsxWasm, isXlsxWasmReady, xlsxToWorkbook } from 'cellify';

// Initialize WASM at app startup (optional but recommended)
await initXlsxWasm();

// Check if WASM is available
if (isXlsxWasmReady()) {
  console.log('Using WASM-accelerated parser');
}

// Import will automatically use WASM if available
const { workbook } = xlsxToWorkbook(buffer);

// Disable WASM for specific imports if needed
const { workbook } = xlsxToWorkbook(buffer, { useWasm: false });

Performance Comparison

| File Size | JS Parser | WASM Parser | Speedup | |-----------|-----------|-------------|---------| | 10K cells | ~500ms | ~50ms | 10x | | 100K cells| ~5s | ~200ms | 25x | | 1M cells | ~50s | ~2s | 25x |

Building WASM (Optional)

The WASM module is pre-built and included. To rebuild from source:

# Install Rust
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

# Add WASM target
rustup target add wasm32-unknown-unknown

# Install wasm-pack
cargo install wasm-pack

# Build WASM module
npm run build:wasm

CSV Import/Export

import { sheetToCsv, csvToWorkbook } from 'cellify';

// Export to CSV
const csv = sheetToCsv(sheet);
// "Product,Units,Price,Total\r\nLaptop,150,999.99,149998.5"

// Import from CSV (auto-detects delimiter)
const { workbook } = csvToWorkbook(csvString);

// With options
const csv = sheetToCsv(sheet, {
  delimiter: ';',
  includeHeaders: true,
  dateFormat: 'YYYY-MM-DD',
});

Cell Styling

// Font styling
cell.style = {
  font: {
    name: 'Arial',
    size: 14,
    bold: true,
    italic: true,
    underline: 'single', // 'single' | 'double'
    strikethrough: true,
    color: '#FF0000',
  },
};

// Fill/background
cell.style = {
  fill: {
    type: 'pattern',
    pattern: 'solid',
    foregroundColor: '#FFFF00',
  },
};

// Borders
cell.style = {
  borders: {
    top: { style: 'thin', color: '#000000' },
    right: { style: 'medium', color: '#000000' },
    bottom: { style: 'thick', color: '#000000' },
    left: { style: 'dashed', color: '#000000' },
  },
};

// Alignment
cell.style = {
  alignment: {
    horizontal: 'center', // 'left' | 'center' | 'right'
    vertical: 'middle',   // 'top' | 'middle' | 'bottom'
    wrapText: true,
    textRotation: 45,     // -90 to 90 degrees
  },
};

// Number format
cell.style = {
  numberFormat: {
    formatCode: '#,##0.00', // Excel format codes
  },
};

// Apply style to range
sheet.applyStyle('A1:D1', {
  font: { bold: true },
  fill: { type: 'pattern', pattern: 'solid', foregroundColor: '#E0E0E0' },
});

Cell Merging

// Merge cells using A1 notation
sheet.mergeCells('A1:D1'); // Merge A1 through D1

// Merge cells using coordinates
sheet.mergeCells({ startRow: 0, startCol: 0, endRow: 2, endCol: 2 });

// Check if cell is part of a merge
const cell = sheet.cell('B1');
if (cell.isMergeMaster) {
  console.log('This is the top-left cell of a merge');
}
if (cell.isMerged) {
  console.log('This cell is merged into another cell');
}

// Unmerge cells
sheet.unmergeCells('A1:D1');

Row and Column Configuration

// Set column width (in characters)
sheet.setColumnWidth(0, 20);  // Column A = 20 characters wide
sheet.setColumnWidth('B', 15); // Column B = 15 characters

// Set row height (in points)
sheet.setRowHeight(0, 30); // Row 1 = 30 points tall

// Hide rows/columns
sheet.hideRow(5);
sheet.hideColumn('C');

// Get all column configs
sheet.columns.forEach((config, index) => {
  console.log(`Column ${index}: width=${config.width}`);
});

Freeze Panes

// Freeze first row (header)
sheet.freeze(1, 0);

// Freeze first column
sheet.freeze(0, 1);

// Freeze both (first row and first two columns)
sheet.freeze(1, 2);

Formulas

// Set a formula
sheet.cell('D2').setFormula('B2*C2');

// With or without leading '='
sheet.cell('D3').setFormula('=SUM(B2:B10)');

// Access formula text
const formula = sheet.cell('D2').formula;
console.log(formula.formula); // 'B2*C2'

// Note: Cellify stores formulas but doesn't evaluate them yet
// Excel will calculate values when the file is opened

Cell Comments

// Add a comment to a cell
sheet.cell('A1').setComment('This needs review', 'John Doe');

// Add comment without author
sheet.cell('B1').setComment('Important note');

// Access comment
const cell = sheet.cell('A1');
if (cell.comment) {
  console.log(cell.comment.text);   // 'This needs review'
  console.log(cell.comment.author); // 'John Doe'
}

// Clear comment
sheet.cell('A1').clearComment();
// or
sheet.cell('A1').comment = undefined;

Hyperlinks

sheet.cell('A1').value = 'Visit Google';
sheet.cell('A1').setHyperlink('https://google.com', 'Go to Google');

sheet.cell('A2').value = 'Contact Us';
sheet.cell('A2').setHyperlink('mailto:[email protected]');

sheet.cell('A3').value = 'Go to Summary';
sheet.cell('A3').setHyperlink('#Summary!A1', 'Jump to Summary sheet');

const cell = sheet.cell('A1');
if (cell.hyperlink) {
  console.log(cell.hyperlink.target);  // 'https://google.com'
  console.log(cell.hyperlink.tooltip); // 'Go to Google'
}

sheet.cell('A1').clearHyperlink();

Named Ranges

// Define a named range
workbook.addDefinedName('SalesData', 'Sheet1!$A$1:$D$100');

// Use in formulas
sheet.cell('E1').setFormula('SUM(SalesData)');

Workbook Properties

const workbook = new Workbook();

// Set metadata
workbook.title = 'Q4 Sales Report';
workbook.author = 'John Doe';
workbook.setProperties({
  subject: 'Quarterly Sales',
  company: 'Acme Corp',
  keywords: ['sales', 'q4', '2024'],
});

// These appear in Excel's File > Info panel

Accessibility

Cellify provides helpers for building accessible spreadsheet UIs:

import {
  getCellAccessibility,
  getAriaAttributes,
  announceNavigation,
} from 'cellify';

// Generate ARIA attributes for a cell
const a11y = getCellAccessibility(cell, sheet, {
  headerRows: 1,
  headerCols: 1,
});

const ariaProps = getAriaAttributes(a11y);
// { role: 'gridcell', 'aria-colindex': 2, 'aria-rowindex': 3, ... }

// Screen reader announcement
const announcement = announceNavigation(cell);
// { message: 'Cell B3, row 3, column 2, 1500', priority: 'polite' }

API Reference

Workbook

| Method | Description | |--------|-------------| | addSheet(name?) | Add a new sheet | | getSheet(name) | Get sheet by name | | getSheetByIndex(index) | Get sheet by index | | removeSheet(sheet) | Remove a sheet | | renameSheet(old, new) | Rename a sheet |

Sheet

| Method | Description | |--------|-------------| | cell(address) | Get cell by A1 notation | | cell(row, col) | Get cell by coordinates | | mergeCells(range) | Merge cells in range | | unmergeCells(range) | Unmerge cells | | setColumnWidth(col, width) | Set column width | | setRowHeight(row, height) | Set row height | | freeze(rows, cols) | Freeze panes | | setAutoFilter(range) | Enable auto filter | | applyStyle(range, style) | Apply style to range |

Cell

| Property | Description | |----------|-------------| | value | Cell value (string, number, boolean, Date) | | style | Cell style object | | formula | Formula object | | comment | Cell comment with text and author | | hyperlink | Hyperlink with target URL and tooltip | | address | A1 notation address | | type | Value type | | isMerged | Is part of merge | | isMergeMaster | Is top-left of merge |

Format Functions

| Function | Description | |----------|-------------| | workbookToXlsx(workbook) | Export to Uint8Array | | workbookToXlsxBlob(workbook) | Export to Blob | | xlsxToWorkbook(buffer) | Import from Uint8Array | | xlsxBlobToWorkbook(blob) | Import from Blob | | sheetToCsv(sheet) | Export sheet to CSV | | csvToWorkbook(csv) | Import CSV to workbook | | initXlsxWasm() | Initialize WASM parser | | isXlsxWasmReady() | Check if WASM is ready |

Browser Usage

<script type="module">
import { Workbook, workbookToXlsxBlob } from 'cellify';

// Create workbook
const workbook = new Workbook();
const sheet = workbook.addSheet('Data');
sheet.cell('A1').value = 'Hello, Excel!';

// Download as file
const blob = workbookToXlsxBlob(workbook);
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'spreadsheet.xlsx';
a.click();
</script>

Supported Cell Types

| Type | Example | Notes | |------|---------|-------| | String | 'Hello' | Plain text | | Number | 42, 3.14 | Integers and decimals | | Boolean | true, false | | | Date | new Date() | Converted to Excel serial | | Formula | '=SUM(A1:A10)' | Stored, evaluated by Excel | | Error | '#VALUE!' | Excel error values |

Border Styles

Available border styles: thin, medium, thick, dashed, dotted, double, hair, dashDot, dashDotDot, mediumDashed, mediumDashDot, mediumDashDotDot, slantDashDot

Planned Features

  • [ ] Formula evaluation engine
  • [ ] Data validation (dropdowns, constraints)
  • [ ] Conditional formatting export
  • [ ] Charts (basic)

Contributing

See CONTRIBUTING.md for development setup and guidelines.

License

MIT - see LICENSE