cellify
v0.3.0
Published
A lightweight, affordable spreadsheet library for building Excel-like experiences
Maintainers
Readme
*Only dependency is
fflatefor 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 demoThe 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

Installation
npm install cellifyQuick 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 fileExcel (.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 browsersExcel (.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:wasmCSV 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 openedCell 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 panelAccessibility
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
