# Spreadsheet
Full spreadsheet editor with formulas, cell styling, multi-sheet support, charts, conditional formatting, named ranges, sorting, filtering, freeze panes, clipboard operations, find/replace, and import/export to XLSX/CSV/ODS/PDF.
## Live Demo
## Embed
```html
```
### Open with a CSV File
```js
var csvUrl = "https://example.com/data.csv";
var src = "https://sgapps.io/online/webapp/spreadsheet/url/" + btoa(csvUrl);
```
---
## Display Modes
The editor supports three pre-configured embedding modes (set via the `mode` constructor option). The wrapping window currently launches in `editor` mode by default but the same instance can be queried via `getMode`:
| Mode | Toolbar | Formula Bar | Sheet Tabs | Editing | Best For |
|---|---|---|---|---|---|
| `"editor"` | shown | shown | shown | enabled | Full spreadsheet application |
| `"viewer"` | hidden | hidden | shown | disabled | Displaying data, reports, dashboards |
| `"minimal"` | hidden | shown | hidden | enabled | Compact data-entry forms, embedded grids |
```js
socket.fire("webapp::instance::request", "getMode",
function (err, mode) { console.log("Mode:", mode); });
```
---
## Events Reference
### Cell Operations
#### `setValue` -- Set Cell Value
```js
socket.fire("webapp::instance::request", "setValue", "A1", "Hello World",
function (err) { console.log(err || "Set"); });
```
#### `setFormula` -- Set Cell Formula
The formula expression is **without** the leading `=` (the editor stores the raw expression). If you'd rather use the `=` prefix, just call `setValue` -- it auto-detects values starting with `=` as formulas.
```js
// Direct formula (no leading =)
socket.fire("webapp::instance::request", "setFormula", "A4", "SUM(A2:A3)",
function (err) { console.log(err || "Formula set"); });
// Or via setValue with = prefix
socket.fire("webapp::instance::request", "setValue", "A4", "=SUM(A2:A3)");
```
#### `getFormula` -- Get Cell Formula
```js
socket.fire("webapp::instance::request", "getFormula", "A4",
function (err, expr) { console.log("Formula:", expr); });
```
#### `setCellStyle` -- Style a Cell
```js
socket.fire("webapp::instance::request", "setCellStyle", "A1", {
bold: true, fontSize: 14, bg: "#4a6cf7", color: "#ffffff"
});
```
The style object uses these properties (any subset is allowed):
| Property | Type | Default | Description |
|---|---|---|---|
| `bold` | `boolean` | `false` | Bold text |
| `italic` | `boolean` | `false` | Italic text |
| `underline` | `boolean` | `false` | Underlined text |
| `strikethrough` | `boolean` | `false` | Strikethrough text |
| `fontFamily` | `string` | `"Arial"` | Font family name |
| `fontSize` | `number` | `11` | Font size in points |
| `color` | `string` | `"#000000"` | Text color (hex) |
| `bg` | `string` | `null` | Background / fill color (hex). `null` = no fill |
| `align` | `string` | `"left"` | Horizontal: `"left"`, `"center"`, `"right"` |
| `valign` | `string` | `"bottom"` | Vertical: `"top"`, `"middle"`, `"bottom"` |
| `wrap` | `boolean` | `false` | Wrap text within cell |
| `numFmt` | `string` | `"General"` | Number format string (see below) |
| `borderTop` / `borderRight` / `borderBottom` / `borderLeft` | `Object` | `null` | `{style, width, color}` -- style is `"solid"`, `"dashed"`, or `"dotted"` |
```js
// Style a header row with bold blue background and a thick bottom border
socket.fire("webapp::instance::request", "setCellStyle", "A1:F1", {
bold: true,
bg: "#1a73e8",
color: "#ffffff",
align: "center",
fontSize: 12,
borderBottom: { style: "solid", width: 2, color: "#0d47a1" }
});
```
#### Number Formats (`numFmt`)
The `numFmt` property controls how a cell value is displayed. Common format strings:
| Format | Example | Description |
|---|---|---|
| `"General"` | `1234.5` | Default — no special formatting |
| `"0"` | `1235` | Integer (rounded) |
| `"0.00"` | `1234.50` | Fixed 2 decimal places |
| `"#,##0"` | `1,235` | Thousands separator |
| `"#,##0.00"` | `1,234.50` | Thousands + 2 decimals |
| `"$#,##0.00"` | `$1,234.50` | US currency |
| `"0%"` | `12%` | Percentage (value × 100) |
| `"0.00%"` | `12.35%` | Percentage with decimals |
| `"0.00E+0"` | `1.23E+3` | Scientific notation |
| `"yyyy-mm-dd"` | `2026-04-04` | ISO date |
| `"mm/dd/yyyy"` | `04/04/2026` | US date |
| `"hh:MM:ss"` | `14:30:00` | Time |
| `"@"` | `(text)` | Text — no number conversion |
```js
socket.fire("webapp::instance::request", "setCellStyle", "B2:B100", { numFmt: "$#,##0.00" }); // Currency
socket.fire("webapp::instance::request", "setCellStyle", "C2:C100", { numFmt: "0.0%" }); // Percentage
socket.fire("webapp::instance::request", "setCellStyle", "D2:D100", { numFmt: "yyyy-mm-dd" }); // Date
```
#### `getValue` -- Get Cell Value
```js
socket.fire("webapp::instance::request", "getValue", "A1",
function (err, cell) { console.log("Value:", cell); });
```
---
### Sheet Management
```js
// Add a new sheet
socket.fire("webapp::instance::request", "addSheet", "Data", function (err) {});
// Switch to sheet (0-based)
socket.fire("webapp::instance::request", "activeSheet", 1, function (err) {});
// Read current active sheet
socket.fire("webapp::instance::request", "activeSheet",
function (err, idx) { console.log("Active:", idx); });
// Rename sheet
socket.fire("webapp::instance::request", "renameSheet", 0, "Summary", function (err) {});
// Delete sheet
socket.fire("webapp::instance::request", "deleteSheet", 1, function (err) {});
```
---
### Row & Column Operations
```js
// Insert 2 rows starting at row index 5
socket.fire("webapp::instance::request", "insertRow", 5, 2, function (err) {});
// Insert a column at column 0 (leftmost)
socket.fire("webapp::instance::request", "insertCol", 0, 1, function (err) {});
// Delete 3 rows starting at index 10
socket.fire("webapp::instance::request", "deleteRow", 10, 3, function (err) {});
// Delete a single column
socket.fire("webapp::instance::request", "deleteCol", 4, function (err) {});
```
---
### Selection
```js
// Select a single cell
socket.fire("webapp::instance::request", "setSelection", "B2");
// Select a range
socket.fire("webapp::instance::request", "setSelection", "A1:D10");
// Read current selection
socket.fire("webapp::instance::request", "getSelection",
function (err, sel) {
console.log(sel.rangeStr); // e.g. "A1:D10"
console.log(sel.activeCell);
});
```
---
### Sort, Filter, Find & Replace
```js
// Sort range A1:D100 by column 0 ascending, then by column 2 descending
socket.fire("webapp::instance::request", "sort", "A1:D100",
[{col: 0, ascending: true}, {col: 2, ascending: false}],
function (err) {});
// Toggle auto-filter for current selection
socket.fire("webapp::instance::request", "filter", function (err) {});
// Find all matches
socket.fire("webapp::instance::request", "find", "Total",
{ matchCase: false, wholeCell: false, regex: false },
function (err, matches) {
// matches: [{ ref: "A2", sheet: 0, value: "Total" }, ...]
console.log("Found", matches.length, "matches");
});
// Find with a regex (treats query as a regular expression)
socket.fire("webapp::instance::request", "find", "\\d{4}-\\d{2}-\\d{2}",
{ regex: true });
// Replace
socket.fire("webapp::instance::request", "replace", "old", "new", { matchCase: true },
function (err, count) { console.log("Replaced", count); });
```
**Find / Replace options:**
| Option | Type | Default | Description |
|---|---|---|---|
| `matchCase` | `boolean` | `false` | Case-sensitive search |
| `wholeCell` | `boolean` | `false` | Match entire cell content (not substring) |
| `regex` | `boolean` | `false` | Treat the query as a regular expression |
| `sheet` | `number` | all | Search only in a specific sheet index |
---
### Clipboard
```js
// Copy current selection
socket.fire("webapp::instance::request", "copy");
// Or specify a range
socket.fire("webapp::instance::request", "copy", "A1:B5");
// Cut
socket.fire("webapp::instance::request", "cut", "A1:B5");
// Paste at active cell
socket.fire("webapp::instance::request", "paste");
// Paste mode: "all" | "values" | "formats" | "formulas" | "transpose"
socket.fire("webapp::instance::request", "paste", "D1", "values");
// Copy from one place to another in one call
socket.fire("webapp::instance::request", "copyRange", "A1:B5", "D1");
```
---
### Merge, Freeze & Layout
```js
// Merge cells
socket.fire("webapp::instance::request", "merge", "A1:C1");
// Unmerge
socket.fire("webapp::instance::request", "unmerge", "A1:C1");
// Freeze first row + first 2 columns
socket.fire("webapp::instance::request", "freeze", 1, 2);
// Unfreeze
socket.fire("webapp::instance::request", "freeze", 0, 0);
```
---
### Charts
Charts render as inline SVG and can be dragged/resized after insertion. Supported chart types: `"bar"`, `"line"`, `"pie"`, `"area"`, `"scatter"`, `"combo"`.
```js
// Bar chart
socket.fire("webapp::instance::request", "insertChart", "A1:B7", "bar",
{ title: "Sales by Region", legend: "right", width: 400, height: 300 });
// Line chart
socket.fire("webapp::instance::request", "insertChart", "A1:C12", "line",
{ title: "Monthly Trend" });
// Pie chart
socket.fire("webapp::instance::request", "insertChart", "A1:B5", "pie",
{ title: "Market Share" });
// Scatter
socket.fire("webapp::instance::request", "insertChart", "A1:B50", "scatter",
{ title: "Correlation" });
// Area
socket.fire("webapp::instance::request", "insertChart", "A1:D12", "area",
{ title: "Revenue Breakdown" });
```
---
### Named Ranges
Define names for cells or ranges to use them in formulas:
```js
// Define
socket.fire("webapp::instance::request", "namedRange", "tax_rate", "B1");
socket.fire("webapp::instance::request", "namedRange", "prices", "Sheet1!B2:B100");
// Use in a formula
socket.fire("webapp::instance::request", "setValue", "C2", "=B2*tax_rate");
socket.fire("webapp::instance::request", "setValue", "D1", "=SUM(prices)");
// Read a named range
socket.fire("webapp::instance::request", "namedRange", "tax_rate",
function (err, ref) { console.log(ref); });
```
---
### Conditional Formatting
Apply visual rules to highlight cells based on their values. Each rule is an object with a `type` discriminator.
```js
// Highlight values greater than 1000 in red
socket.fire("webapp::instance::request", "conditionalFormat", "B2:B100", {
type: "cellIs",
operator: "greaterThan",
values: [1000],
style: { bg: "#fce4ec", color: "#c62828" }
});
// 3-color scale (green -> yellow -> red)
socket.fire("webapp::instance::request", "conditionalFormat", "C2:C100", {
type: "colorScale",
colors: ["#4caf50", "#ffeb3b", "#f44336"]
});
// Data bars
socket.fire("webapp::instance::request", "conditionalFormat", "D2:D50", {
type: "dataBar",
color: "#2196f3"
});
// Icon sets: 'arrows3', 'traffic3', 'stars3', 'flags3'
socket.fire("webapp::instance::request", "conditionalFormat", "E2:E50", {
type: "iconSet",
icons: "arrows3"
});
// Formula-based rule (alternate row shading)
socket.fire("webapp::instance::request", "conditionalFormat", "A2:F100", {
type: "expression",
formula: "=MOD(ROW(),2)=0",
style: { bg: "#f5f5f5" }
});
```
**Rule types:** `"cellIs"`, `"colorScale"`, `"dataBar"`, `"iconSet"`, `"expression"`
**Operators (for `cellIs`):** `"greaterThan"`, `"lessThan"`, `"equal"`, `"notEqual"`, `"greaterThanOrEqual"`, `"lessThanOrEqual"`, `"between"`, `"notBetween"`
---
### Import / Export
```js
// Export as CSV (active sheet)
socket.fire("webapp::instance::request", "exportCSV",
function (err, csv) { console.log(csv); });
// Export sheet by index
socket.fire("webapp::instance::request", "exportCSV", 0,
function (err, csv) { console.log(csv); });
// Export as XLSX (Promise -> Blob)
socket.fire("webapp::instance::request", "exportXLSX",
function (err, blob) {
var url = URL.createObjectURL(blob);
// download or display
});
// Export as ODS / PDF
socket.fire("webapp::instance::request", "exportODS", function (err, blob) {});
socket.fire("webapp::instance::request", "exportPDF", function (err, blob) {});
// Full state as JSON
socket.fire("webapp::instance::request", "toJSON",
function (err, data) { console.log(data); });
// Restore state from JSON
socket.fire("webapp::instance::request", "fromJSON", savedData);
// Import a file (XLSX, CSV, ODS, ...)
var file = /* a File or Blob */;
socket.fire("webapp::instance::request", "importFile", file,
function (err) { if (!err) console.log("Imported"); });
```
#### JSON data model
`toJSON` returns -- and `fromJSON` accepts -- this workbook structure. You can save it to localStorage / IndexedDB / a server, then restore it later in any spreadsheet instance:
```json
{
"name": "Workbook",
"sheets": [
{
"name": "Sheet1",
"tabColor": null,
"rows": { "5": { "height": 30, "hidden": false } },
"cols": { "2": { "width": 150, "hidden": false } },
"cells": {
"A1": { "v": "Revenue", "s": { "bold": true, "bg": "#4a6cf7", "color": "#fff" } },
"B1": { "v": 50000, "f": null, "s": { "numFmt": "$#,##0" } },
"B2": { "v": null, "f": "B1*1.1", "s": {} }
},
"merges": ["A1:C1"],
"freeze": { "row": 1, "col": 0 },
"filters": null,
"charts": [],
"conditionalFormats": [],
"namedRanges": { "total": "B10" },
"validations": []
}
],
"activeSheet": 0
}
```
**Cell object schema:** `v` = raw value, `f` = formula expression (without `=`, or `null`), `s` = style object (see [Cell Styles](#setcellstyle--style-a-cell)).
> **Tip:** the engine recalculates all formulas when you call `fromJSON`, so cells with `v: null, f: "..."` are filled in automatically. You don't need to persist computed values.
```js
// Save to localStorage on every cell change
socket.on("api-event::instance:event:cell-change", function () {
socket.fire("webapp::instance::request", "toJSON", function (err, data) {
if (!err) localStorage.setItem("myWorkbook", JSON.stringify(data));
});
});
// Restore on page load
var saved = localStorage.getItem("myWorkbook");
if (saved) {
socket.fire("webapp::instance::request", "fromJSON", JSON.parse(saved));
}
```
---
### View Controls
```js
socket.fire("webapp::instance::request", "zoom", 1.5); // 150%
socket.fire("webapp::instance::request", "readOnly", true); // read-only mode
socket.fire("webapp::instance::request", "freeze", 1, 1); // freeze first row + col
socket.fire("webapp::instance::request", "print"); // open print dialog
```
---
### Built-in Dialogs
The Spreadsheet ships with 9 dialogs that can be opened remotely. Each one is a fully interactive form -- the user fills it in and the changes are applied automatically.
```js
socket.fire("webapp::instance::request", "showFormatCellsDialog");
socket.fire("webapp::instance::request", "showSortDialog");
socket.fire("webapp::instance::request", "showFindDialog", true); // true = find/replace
socket.fire("webapp::instance::request", "showConditionalFormatDialog");
socket.fire("webapp::instance::request", "showDataValidationDialog");
socket.fire("webapp::instance::request", "showNamedRangeDialog");
socket.fire("webapp::instance::request", "showChartDialog");
socket.fire("webapp::instance::request", "showFunctionWizard");
socket.fire("webapp::instance::request", "showPrintDialog");
```
| Dialog | Description |
|---|---|
| **Format Cells** (`Ctrl+1`) | 5 tabs: Number, Alignment, Font, Border, Fill |
| **Sort** | Multi-level sort with column/direction selectors |
| **Find & Replace** (`Ctrl+F`/`Ctrl+H`) | Search with match case, whole cell, regex options |
| **Conditional Format** | Rule builder: cell value, formula, color scale, data bar, icon set |
| **Data Validation** | 3 tabs: Settings (type/operator/values), Input Message, Error Alert |
| **Named Range Manager** | List, add, edit, delete named ranges |
| **Insert Chart** | Chart type grid with preview, title/legend settings |
| **Function Wizard** | Category filter, function list, argument builder (also reachable from the `fx` button in the formula bar) |
| **Print** (`Ctrl+P`) | Print area, orientation, margins, scale, headers/footers |
#### Data Validation rule shape
Inside the Data Validation dialog (or for users embedding the editor and calling validation programmatically through their own code) the rule shape is:
```js
// Dropdown list
{ type: "list", list: ["High", "Medium", "Low"], message: "Select a priority" }
// Number range
{ type: "number", operator: "between", value1: 0, value2: 100, message: "0 to 100" }
// Date range
{ type: "date", operator: "greaterThan", value1: "2026-01-01" }
// Text length
{ type: "textLength", operator: "lessThanOrEqual", value1: 50 }
// Custom formula
{ type: "custom", formula: "=AND(F2>=0, MOD(F2,1)=0)", message: "Positive integer" }
```
**Validation types:** `"list"`, `"number"`, `"date"`, `"textLength"`, `"custom"`
**Validation operators:** `"between"`, `"notBetween"`, `"equal"`, `"notEqual"`, `"greaterThan"`, `"lessThan"`, `"greaterThanOrEqual"`, `"lessThanOrEqual"`
---
### Listening to Editor Events
The Spreadsheet forwards internal events to the embedder via `api-event::instance:`. The first element of the `args` array is the event payload.
| Event | Payload | Description |
|---|---|---|
| `event:ready` | -- | Spreadsheet fully initialized and rendered |
| `event:cell-change` | `{ref, oldValue, newValue, sheet}` | A cell value or formula was modified |
| `event:recalculate` | -- | Formulas were recalculated |
| `event:sheet-change` | `{index, name}` | The active sheet was switched (or a sheet was added/deleted/renamed) |
| `event:selection-change` | `{start: {row, col}, end: {row, col}, sheet}` | Selection range changed |
> **Editor-only events:** the underlying library also emits `event:before-edit`, `event:after-edit`, `event:context-menu`, `event:scroll`, `event:zoom`, `event:import` and `event:export`. These are not currently forwarded by the wrapper but can be added on request.
```js
// Cell value or formula changed
socket.on("api-event::instance:event:cell-change", function (args) {
var data = args[0];
console.log("Cell " + data.ref + " : " + data.oldValue + " -> " + data.newValue);
});
// Recalculation finished
socket.on("api-event::instance:event:recalculate", function () {
console.log("Formulas recalculated");
});
// Sheet added/deleted/renamed
socket.on("api-event::instance:event:sheet-change", function (args) {
var s = args[0];
console.log("Active sheet:", s.index, s.name);
});
// Selection moved
socket.on("api-event::instance:event:selection-change", function (args) {
var sel = args[0];
console.log("From", sel.start, "to", sel.end);
});
// Editor finished initial load
socket.on("api-event::instance:event:ready", function () {
console.log("Spreadsheet ready");
});
```
---
## Keyboard Shortcuts
Whenever the spreadsheet has focus, the following keyboard shortcuts are available:
| Shortcut | Action |
|---|---|
| `Enter` | Confirm edit, move down |
| `Tab` / `Shift+Tab` | Confirm edit, move right / left |
| `Escape` | Cancel edit |
| `F2` | Enter edit mode on selected cell |
| `Delete` | Clear cell content |
| `Ctrl+Z` | Undo |
| `Ctrl+Y` / `Ctrl+Shift+Z` | Redo |
| `Ctrl+C` / `Ctrl+X` / `Ctrl+V` | Copy / Cut / Paste |
| `Ctrl+D` | Fill down |
| `Ctrl+R` | Fill right |
| `Ctrl+B` / `Ctrl+I` / `Ctrl+U` | Bold / Italic / Underline |
| `Ctrl+A` | Select all cells |
| `Ctrl+F` | Open Find dialog |
| `Ctrl+H` | Open Find & Replace dialog |
| `Ctrl+G` | Go to cell dialog |
| `Ctrl+Home` / `Ctrl+End` | Navigate to A1 / last used cell |
| `Ctrl+;` / `Ctrl+Shift+;` | Insert current date / time |
| `Ctrl+1` | Open Format Cells dialog |
| `Ctrl+Shift+L` | Toggle auto-filter |
| `Arrow keys` | Move selection |
| `Ctrl+Arrow` | Jump to edge of data region |
| `Shift+Arrow` | Extend selection |
| `Shift+Click` / `Ctrl+Click` | Extend selection / multi-selection |
| `Page Up` / `Page Down` | Scroll by one page |
| `Ctrl+P` | Print current sheet |
| `Alt+Enter` | Insert new line inside a cell |
---
## Formula Functions
The formula engine supports 200+ functions. Use them with `setValue` (with leading `=`) or `setFormula` (without leading `=`):
```js
socket.fire("webapp::instance::request", "setValue", "D2", "=B2*C2");
socket.fire("webapp::instance::request", "setValue", "D10", "=SUM(D2:D9)");
socket.fire("webapp::instance::request", "setValue", "E2", '=IF(D2>1000,"High","Low")');
socket.fire("webapp::instance::request", "setValue", "F2", "=VLOOKUP(A2,Sheet2!A:B,2,FALSE)");
socket.fire("webapp::instance::request", "setValue", "G2", '=TEXT(B2,"$#,##0.00")');
socket.fire("webapp::instance::request", "setValue", "H2", '=IFERROR(B2/C2,"N/A")');
```
**Available categories:**
- **Math:** `SUM`, `AVERAGE`, `MIN`, `MAX`, `COUNT`, `COUNTA`, `ROUND`, `CEILING`, `FLOOR`, `POWER`, `SQRT`, `MOD`, `RAND`, `RANDBETWEEN`, `PI`, `LOG`, `LN`, `EXP`, `INT`, `SIGN`, `TRUNC`, `PRODUCT`, `SUMPRODUCT`, `SIN`, `COS`, `TAN`, `ASIN`, `ACOS`, `ATAN`, `ATAN2`, `DEGREES`, `RADIANS`, `ABS`
- **Conditional:** `SUMIF`, `SUMIFS`, `COUNTIF`, `COUNTIFS`, `AVERAGEIF`, `AVERAGEIFS`
- **Text:** `CONCAT`, `CONCATENATE`, `LEFT`, `RIGHT`, `MID`, `LEN`, `UPPER`, `LOWER`, `PROPER`, `TRIM`, `SUBSTITUTE`, `REPLACE`, `FIND`, `SEARCH`, `TEXT`, `VALUE`, `REPT`, `CHAR`, `CODE`, `EXACT`, `T`, `TEXTJOIN`
- **Logical:** `IF`, `AND`, `OR`, `NOT`, `XOR`, `IFERROR`, `IFNA`, `IFS`, `SWITCH`, `TRUE`, `FALSE`
- **Lookup:** `VLOOKUP`, `HLOOKUP`, `INDEX`, `MATCH`, `OFFSET`, `INDIRECT`, `ROW`, `COLUMN`, `ROWS`, `COLUMNS`, `ADDRESS`, `CHOOSE`
- **Date / Time:** `NOW`, `TODAY`, `DATE`, `YEAR`, `MONTH`, `DAY`, `HOUR`, `MINUTE`, `SECOND`, `DATEVALUE`, `DAYS`, `EDATE`, `EOMONTH`, `WEEKDAY`, `WEEKNUM`
- **Statistical:** `MEDIAN`, `MODE`, `STDEV`, `VAR`, `LARGE`, `SMALL`, `RANK`, `PERCENTILE`, `QUARTILE`, `CORREL`, `FORECAST`
- **Financial:** `PMT`, `FV`, `PV`, `NPV`, `IRR`, `NPER`
- **Info:** `ISBLANK`, `ISERROR`, `ISNUMBER`, `ISTEXT`, `ISLOGICAL`, `ISNA`, `TYPE`, `NA`, `ERROR.TYPE`
#### Formula error values
| Error | Meaning |
|---|---|
| `#REF!` | Invalid cell reference |
| `#VALUE!` | Wrong value type |
| `#DIV/0!` | Division by zero |
| `#NAME?` | Unrecognized function or name |
| `#N/A` | Value not available |
| `#NULL!` | Invalid range intersection |
| `#NUM!` | Invalid numeric value |
| `#CIRC!` | Circular reference detected |
---
## Complete Example
```html
```