The spreadsheet shell

Everything between the ribbon and the sheet tabs, as one component.

<script>
  import { SvSheet, createWorkbook } from '@svgrid/enterprise'

  const wb = createWorkbook([
    { name: 'Budget', cells: [['Line', 'Jan'], ['Rent', '2400']] },
  ])
</script>

<SvSheet workbook={wb} />

Open the live example: Spreadsheet + Ribbon bar (Spreadsheet)

That is the whole required API. <SvSheet /> with no props at all opens an empty single-sheet workbook, which is what "open a spreadsheet" means.

This page is the reference: every prop, every dialog, the file formats and the limits. The tutorials walk through the same ground a task at a time, with a live sheet under each step: Getting started, Formulas, Formatting, Data validation, Files, Data tools, Charts, sparklines and pictures and Review and share.

What you get

Part Comes from
Ribbon: File, Home, Insert, Formulas, Data, Review, View SvSheetRibbon
Name Box and fx bar, showing the RAW text of the active cell SvFormulaBar
A..Z headers over the built-in 1..N row gutter SvGrid
Sheet tabs: switch, rename, reorder, add, duplicate, hide and unhide SvSheetTabs
Sum / Average / Count of the selection the shell
Every Excel shortcut enableSheet()

This is composition, not a second grid. All of it already existed and was being re-assembled by hand in four separate demos. What the component adds is the wiring BETWEEN the parts, done once:

The ribbon is Excel's ribbon

The layout is Excel's, down to the geometry: a group is three rows of 24px, a large button (icon over label) fills a column of that height and small buttons stack three to a column, so Cut / Copy / Format Painter line up beside a tall Paste, as icons alone the way Excel draws them. The Home tab runs Clipboard, Font, Alignment, Number, Cells, Styles, Editing in Excel's order, with Excel's names on the buttons (Undo and Redo open the Editing group as a column of small icons), a dialog-box launcher in the corner of the groups that have a dialog, and the group label underneath, which is what makes a ribbon read as a ribbon rather than a toolbar.

Some things worth knowing are in there because Excel users reach for them without looking:

It is responsive the way Excel's is, in Excel's two steps. When the band is narrower than its groups, the groups go compact from the right first: their small buttons keep the icon and drop the label, so Fill Down becomes an arrow and Wrap Text a glyph, with the title still in the tooltip. Only when every group is compact and the band is still too narrow do groups fold, again from the right, into one large button each (the group's own icon over its name and a chevron) that opens the group in a dropdown. Nothing is hidden, only folded; widen the window and it all comes back in the same order. Every width is measured off an invisible copy of the band, never estimated, so a group folds exactly when it would not fit. On a phone the folded band scrolls sideways.

It collapses the way Excel's does. Double-click a tab, press the chevron at the band's end, or Ctrl+F1, and the band goes away leaving the tab row. A click on a tab then shows that tab's band over the sheet until a command runs, Escape, or a click elsewhere; the pin at its end, a double-click or Ctrl+F1 brings the band back for good. The state is the collapsed prop of SvSheetRibbon, bindable, and the action is raised as toggle-ribbon so onAction can watch or take it over.

The ribbon and the keyboard are the same thing

Click a bold cell and the Bold button lights up. Press Ctrl+B and it goes out. Neither is reacting to the other: both call one function in sheet/shortcuts.ts, and the button's pressed state is read from the format store rather than tracked separately.

That is why sheet/ribbon.ts is a table of data rather than markup. An item either carries a run that calls a real action, or an emits naming something the ribbon cannot do alone:

{ id: 'bold', label: 'B', title: 'Bold', keys: 'Ctrl+B',
  kind: 'toggle',
  run: (cmd) => toggleFormat(cmd, 'bold'),
  isOn: (cmd) => everyCellHas(cmd, (e) => e?.bold === true) }

Buttons that open a dialog set emits and raise an action. Every action reaches your onAction handler first; return true to take it over, otherwise the shell's own dialog opens:

<SvSheet
  {wb}
  onAction={(action, cmd) => {
    if (action === 'goal-seek') { openMyGoalSeekDialog(cmd); return true }
  }}
/>

The shell answers everything on the ribbon itself: recalculate, toggle-formulas (Excel's Ctrl+ `), toggle-filter, sort-asc / sort-desc / sort-custom, and the dialogs listed below. One button has nothing behind it in the library, Insert > Table, and is left off the ribbon unless extras={['insert-table']} says the application answers it.

Props

Prop Default What it does
document one built from workbook or data The sheet document: the workbook plus every sheet's formats, sizes, hidden lines, frozen panes, comments, protection, validation and conditional formatting. Build it with createSheetDocument to save, restore or listen to it outside the component.
workbook a new empty one The workbook to edit, when not supplying a document.
data Seed sheets, when not supplying a workbook.
rows / columns 50 / 12 Minimum size, so a sparse sheet still looks like a sheet.
height 420 Grid viewport height.
columnWidth 96 Default column width.
rowHeight 22 Row height. Excel's is 20 at 100%; 22 fits the 13px cell font.
look 'theme' theme paints the shell with the host's --sg-* tokens, so it follows the app's theme; excel pins Excel's own palette and geometry, light or dark with the page.
columnWidths Per-column overrides keyed by letter: { A: 150 }.
formats Cell formats keyed by address: { B2: { bold: true } } for the sheet active at mount, { 'Orders!F2': ... } for another sheet.
showRibbon / showFormulaBar / showTabs / showStatusBar true Hide any part of the chrome.
onAction Every ribbon action, first; return true to take one over.
extras [] Whether to show Insert > Table, because the application answers it.
commentAuthor Who signs a new comment and a reply, with the time; unset, a new comment is a plain note.
localization { locale, text }: the language of the chrome. text overrides any of the shell's strings (see Localisation); locale formats the status bar's numbers and reaches the grid underneath.
onReady The SvGridApi and the document, once the grid has mounted.
onChange Every change the user lands, once per tick: cells, formats, sizes, hidden, freeze, sheets, comments, protection, page-setup, objects, validation, conditional-formats, structure (with the insert or delete), restore. Undo and redo report too.

formats matters more than it looks: without it a sheet can only be formatted by hand after it loads, which makes it impossible to ship a document that opens the way it was saved.

A bare address formats the sheet that is active when the shell mounts. Qualify it to reach another sheet, the way a formula would - Orders!F2, or 'Price list'!C2 when the name has a space:

<SvSheet
  workbook={wb}
  columnWidths={{ A: 150 }}
  formats={{
    A1: { bold: true, fill: '#e2e8f0', color: '#0f172a' },
    E2: { numFmt: '#,##0' },
    B13: { numFmt: '0.0%' },
    'Orders!F2': { numFmt: '$#,##0.00' },
  }}
/>

Open the live example: Sales report workbook (Spreadsheet)

Files

The File tab is the part of Excel's a document in a page can do: New, Open, Save As, Save As ODS, Save As XLS, Export CSV and Print. Open takes an .xlsx, an .ods, an .xls or a .csv from disk - the kind is read from the bytes, not from the name - and replaces the document with everything the file holds that the document keeps (cells with their formulas, formats, widths and heights, hidden lines and sheets, frozen panes, merges, the filter with its criteria and the rows it folds, validation, conditional formatting, protection, comments, names, the active sheet); Save As downloads the document as an .xlsx that Excel, Google Sheets and LibreOffice open with the same parts; Save As ODS writes the same document in OpenDocument, the format LibreOffice Calc saves by default and one of the files Google Sheets hands back, with the cells, their formulas in ODF's own reference grammar, the number formats, the looks, widths and heights, merges, hidden lines, links and notes; Save As XLS writes an Excel 97-2003 workbook, the binary .xls that every Excel since 1997 opens and that a bank or an ERP export still often is; Export CSV downloads the active sheet as its cells show, and Open reads such a file back: the separator is guessed, so a comma, a semicolon or a tab file all read as themselves, a quoted field keeps its commas and line breaks, and a field that reads as a percentage, a currency amount or a grouped number lands as the number with the format it implies. A CSV field is never read as a formula, as it is not in Excel; New starts over with one empty sheet, asking first when the sheets hold anything; Print (Ctrl+P) opens the active sheet in the browser's print dialog as its Page Layout says (below). documentToXlsx and documentFromXlsx are the two halves, in @svgrid/enterprise/sheet, and need the jszip peer; documentToOds and documentFromOds are the same pair for OpenDocument; documentToXls and documentFromXls are the pair for the old binary format, and need nothing, since an .xls is not a zip; and documentFromFile takes any of the four and decides by the bytes. csvText and sheetStateFromCsv are the CSV pair, and need nothing either.

What an .ods carries is the document this package keeps, not every corner of either format: cells and their formulas (translated both ways, so =SUM(A1:A3) here is of:=SUM([.A1:.A3]) there, and a structured reference becomes the rectangle it names, since ODF has none), number formats, the cell looks, column widths and row heights, merges, hidden rows and columns, the filter with its values and conditions (a date period or a colour has no ODF spelling, so its rows go out hidden instead), hyperlinks, notes, defined names and sheet protection. Charts, images, sparklines, validation, conditional formatting and frozen panes travel in the .xlsx and not yet in the .ods. A PivotTable travels in every format as the cells it wrote, never as a definition: the .xlsx has no pivot part, so Excel sees the block, not a pivot to refresh.

The .xls is the oldest of the three and carries the least. Cells and their formulas travel, with the formulas as the RPN tokens Excel 97 stored rather than as text: a function that format never had (XLOOKUP, LET, TEXTJOIN, a structured reference) has no token, so such a cell goes out as the value it worked out, and a workbook opened from an .xls reads those the same way round - Excel writes _xlfn.XLOOKUP as a defined name and calls it, and that name turns back into the function here. Number formats, fonts, colours, fills, borders, alignment, column widths, row heights, hidden rows, columns and sheets, merges, frozen panes, sheet protection and defined names travel; charts, pictures, sparklines, validation, conditional formatting and comments do not, since the format kept them somewhere this reader does not go. A filter goes out as the rows it folds, hidden, and nothing else: the file shows the same rows, and the arrows do not come back from it. The sheet is 65,536 rows by 256 columns, and anything past that edge is left out rather than written wrong.

An app that keeps its workbooks somewhere other than the user's disk takes the actions over through onAction (file-open, file-save-xlsx, file-save-ods, file-save-xls, file-new, file-export-csv, file-print) and calls the component's own methods: open(file) replaces the document with an .xlsx, .ods, .xls or .csv Blob, toXlsx() returns the document as a Blob, toOds() the same document in OpenDocument, toXls() it as an Excel 97-2003 workbook, toCsv() the active sheet as text, newWorkbook() empties it, print() opens the print dialog and printHtml() returns the page it would print, for an app that prints its own way.

<SvSheet bind:this={sheet} onAction={(action) => {
  if (action === 'file-save-xlsx') { sheet.toXlsx().then((blob) => upload(blob)); return true }
  if (action === 'file-open') { pickFromServer().then((blob) => sheet.open(blob)); return true }
}} />

Dates are yyyy-mm-dd text in the sheet and serial numbers in the file: a text date goes out as a serial under a date format, and a serial under a date format comes back as text. Tables, charts and images are not carried either way.

Page Layout and Print

The Page Layout tab is Excel's Page Setup group and Sheet Options, per sheet and part of the document: Margins (Normal, Narrow, Wide), Orientation, Size (Letter, Legal, Tabloid, A3, A4, A5), Print Area (Set takes the selection, Clear puts the whole sheet back), Print Titles (the rows that repeat at the top of every page), the group's launcher opening the Page Setup dialog with all of it and the scale, and Print Gridlines and Print Headings. Each change is one undo and reports { kind: 'page-setup' }.

File > Print (Ctrl+P) lays the sheet out as one HTML document and hands it to the browser's own print engine, so fonts, CJK and RTL come out right and nothing is bundled: the print area or the used range, column widths and row heights as the sheet shows them, hidden lines and the rows a filter folds away left out, merges as spans, every cell as it shows with its format and its conditional style, the title rows repeated on each page, the sparklines drawn in their cells and the charts and pictures hung from theirs, a table's header and banding in the colours it wears on the sheet, gridlines and headings when asked, and the orientation, paper and margins in @page. "Save as PDF" in that dialog is the PDF. The setup rides in getState() as pageSetup and in the xlsx as pageSetup, pageMargins, printOptions and Excel's own Print_Area and Print_Titles names, both ways.

Saving and restoring

Everything the user does lands in the sheet's document: the workbook (raw text, formulas as typed, names, the active sheet), and per sheet the formats (locked flags included), column widths, row heights, hidden lines, frozen panes, comments, the protection flag with its allow list and edit ranges, the page setup, the charts and pictures on the sheet, the validation rules and the conditional formatting rules. getState() returns it as plain JSON and setState() puts it back into the same workbook object, so an autosave is an onChange handler and a reload is one call after mount:

<script lang="ts">
  import { SvSheet, type SheetState } from '@svgrid/enterprise'

  let sheet = $state<SvSheet>()
  let timer: ReturnType<typeof setTimeout> | undefined

  function autosave() {
    clearTimeout(timer)
    timer = setTimeout(() => {
      if (sheet) localStorage.setItem('budget', JSON.stringify(sheet.getState()))
    }, 400)
  }

  const saved = localStorage.getItem('budget')
</script>

<SvSheet
  bind:this={sheet}
  onChange={autosave}
  onReady={() => { if (saved) sheet?.setState(JSON.parse(saved) as SheetState) }}
/>

Autosave to a server

The same handler with a fetch in it, plus the three things a network makes you think about:

let timer: ReturnType<typeof setTimeout> | undefined
let saving: Promise<unknown> = Promise.resolve()
let revision = 0

function autosave() {
  clearTimeout(timer)
  // Coalesce a burst of edits into one request. onChange already coalesces
  // per tick; this coalesces per pause in the typing.
  timer = setTimeout(() => {
    const state = sheet!.getState()
    // One request at a time, in order: a save that overtakes its
    // predecessor writes an older document over a newer one.
    saving = saving.then(async () => {
      const response = await fetch(`/api/books/${id}`, {
        method: 'PUT',
        headers: { 'content-type': 'application/json', 'if-match': String(revision) },
        body: JSON.stringify(state),
      })
      if (response.status === 412) return onSomeoneElseSaved()
      revision = Number(response.headers.get('etag') ?? revision + 1)
    })
  }, 800)
}

A save that runs on a timer rather than on onChange is worse, not better: it saves when nothing changed and misses the tab closing. If the tab closing matters, save on visibilitychange as well, with the same snapshot.

onChange is called once per tick with every reason since the last call, so a paste of forty cells is one call, and it says what changed (reasons.some((r) => r.kind === 'structure') for an insert or delete, with the edit on the reason). A change to the workbook's own parts rather than a sheet's, its names or its calculation settings, reports workbook; a table reports tables. A host writing those straight to the workbook raises the same reason with document.changed({ kind: 'workbook' }), which is what puts it on a delta stream. setState clears the grid's undo history, since none of it describes the restored sheet, and reports a single restore.

The same document is available outside the component: build it with createSheetDocument({ sheets }) or createSheetDocument({ state }), pass it as document, and call document.getState(), document.setState() and document.subscribe() on it directly. A document built this way can be saved and restored in Node, with no component at all.

The shape, SheetState (exported with SheetStateEntry, ValidationRule and CfRule):

{
  version: 1,
  workbook: {
    sheets: [{ name, cells }], active, names,
    tables: [{ name: 'Orders', sheet: 'Budget', headerRow: 0, firstCol: 0, lastCol: 4, lastRow: 12, hasTotals: false }],
    iteration: { enabled: true, maxIterations: 100, maxChange: 0.001 },
  },
  sheets: {
    Budget: {
      formats: { 'r0 A': { bold: true, numFmt: '#,##0', locked: false } },
      columnWidths: { A: 150 },
      rowHeights: [[4, 44]],
      hidden: { rows: [7], cols: [] },
      freeze: { rows: 1, cols: 0 },
      comments: { r1: { B: 'Check with finance' }, r4: { D: { text: 'Over budget?', author: 'Ana', at: '2026-03-04T10:00:00.000Z', replies: [{ text: 'By 3%', author: 'Ben', at: '2026-03-04T11:00:00.000Z' }] } } },
      protected: false,
      protection: { allow: { formatRows: true }, ranges: [{ id, title: 'Inputs', rects: [[1, 1, 9, 1]] }] },
      pageSetup: { orientation: 'landscape', paper: 'A4', margins: { top: 0.75, bottom: 0.75, left: 0.7, right: 0.7, header: 0.3, footer: 0.3 }, printArea: null, printTitleRows: [0, 0], gridlines: false, headings: false, scale: 100 },
      objects: [{ id, kind: 'chart', anchor: { row: 12, col: 1, dx: 8, dy: 8, width: 420, height: 260 }, range: [0, 0, 11, 2], type: 'bar', headers: true, series: 'columns' }],
      sparklines: [{ id, location: [1, 6, 9, 6], data: [1, 1, 9, 5], type: 'line', color: '#2563eb' }],
      pivots: [{ id, source: [0, 0, 200, 3], target: { row: 0, col: 8 }, rows: ['Region'], cols: [], values: [{ field: 'Amount', agg: 'sum' }] }],
      links: { r1: { B: { target: 'https://example.com', tip: 'The spec' } } },
      merges: [],
      validation: [{ id, rects, allow: 'whole', operator: 'between', value1: '1', value2: '10', ignoreBlank: true, inCellDropdown: false, alert: { style: 'stop' } }],
      conditionalFormats: [{ id, rects, kind: 'cellIs', operator: 'greater', value1: '50000', style: { fill: '#FFC7CE', color: '#9C0006' } }],
    },
  },
}

Rows are r plus the 0-based index and columns are letters, a format key is the two with a space between; rectangles are [minRow, minCol, maxRow, maxCol], 0-based. A hidden row keeps the height it had before it was hidden, not the 0 it shows while hidden, so an Unhide after a restore brings it back at its size.

A component, not a mode

Kanban, the scheduler and charts are modes of <SvGrid> (board, scheduler, chart) because they are different pictures of the same rows: the grid keeps the data, the row model, sorting and filtering, and the mode draws cards, a calendar or a chart from them. The spreadsheet is the other way round: the same picture, a table, over a different data model. It owns a document (formulas, address-keyed formats, comments, rules) and uses the grid as its rendering primitive, which is why it is a component that composes <SvGrid> rather than a prop on it. installEnterprise() enables it along with everything else; there is nothing extra to register.

Rows into a sheet

sheetCellsFromRows(rows, fields, { totals }) turns an array of records into the cells data or createWorkbook take: a header row of labels, a row per record with each value as the text the engine reads (a number as its digits, a boolean as TRUE or FALSE, a Date as yyyy-mm-dd, nothing for null), and with totals a last row with =SUM(...) under every column that held a number. It is how a grid's rows, or an API's, become a sheet the user can add formulas to; Studio's Spreadsheet block is this over the screen's dataset.

<script lang="ts">
  import { SvSheet, sheetCellsFromRows } from '@svgrid/enterprise'

  const cells = sheetCellsFromRows(orders, [{ field: 'item', label: 'Item' }, 'qty', { field: 'price', label: 'Price' }], { totals: true })
</script>

<SvSheet data={[{ name: 'Orders', cells }]} />

Writing to the workbook from outside

Every edit made through the shell repaints it: typing, the ribbon, a shortcut, the fill handle, and cmd.setCellValue from an onAction handler, which is also how a dialog should write a cell - the write lands in the grid's undo history and the shell sees it.

A write the shell cannot see does not repaint: a Name Manager redefining a name, an import replacing a sheet, a solver applying its answer with wb.setRaw. Call refresh() on the component afterwards:

<script>
  let sheet
</script>

<SvSheet bind:this={sheet} workbook={wb} onAction={(action, cmd) => {
  if (action === 'name-manager') { openNameManager(wb.names, () => sheet.refresh()); return true }
}} />

act(action) runs a ribbon action as if its button had been clicked (sheet.act('sort-asc'), sheet.act('circle-invalid')), for a host with chrome of its own; what the ribbon would raise for onAction is raised here too.

The shell carries Goal Seek, Text to Columns, Remove Duplicates and the Name Manager itself. The demos below each replace one with a dialog of their own, returning true from onAction, and show the write paths a replacement should use: Goal Seek applies through cmd.setCellValue, Text to Columns and Remove Duplicates write inside cmd.batch so each is one Ctrl+Z, and the Name Manager calls refresh().

Open the live example: What-if analysis: Goal Seek (Spreadsheet)

Open the live example: Data cleanup: Text to Columns, Remove Duplicates (Spreadsheet)

Theming

Every part of the shell is painted with the grid's --sg-* tokens, so by default it follows the app's theme the way the grid does: the accent on the selection and the active tab, the header and ribbon grounds, the font and the corner radius are the host's. The icons are one set of line glyphs drawn in the text colour, black on a light ribbon and white on a dark one, as Excel's monochrome set is; the only colour on the ribbon is the bar under Fill Colour and Font Colour, which is the colour the button will apply.

look="excel" pins Excel's palette instead: the shell sets the tokens on its own root to Office's light theme, or its dark theme when the page is dark, so it reads as Excel whatever theme the app around it runs, and the grid inside picks the same tokens up. Nothing else changes: the geometry, the icons and the layout are the same either way.

Using the parts separately

SvSheet is a composition of exported components, so anything it does you can do by hand when you want a different arrangement:

<SvSheetRibbon cmd={() => api?.getCommandContext() ?? null} onChange={refresh} />
<SvFormulaBar {active} value={raw} onCommit={commit} />
<SvGrid ... />
<SvSheetTabs workbook={wb} {version} onChange={refresh} />

api.getCommandContext() is the seam that makes this work. A keyboard command is handed a context on every keystroke; a ribbon button has no keystroke to ride in on, and without it every action would have to be re-implemented against the public api and kept in step by hand.

Formulas > Formula Auditing has Excel's arrows: Trace Precedents draws a blue arrow from each cell the active formula reads to the formula, a dot on the cell read and an arrowhead on the formula, and pressing it again traces one more level out; Trace Dependents draws the arrows the other way, to the formulas that read the active cell, every formula in the workbook counted whether or not it has been on screen; Remove Arrows clears them, as does a switch to another sheet. Cells on other sheets are left out, since there is nothing on this sheet to draw to. The arrows are measured from the rendered cells and follow every scroll and repaint; they are not part of the document. wb.precedents(sheet, row, col) and wb.dependents(sheet, row, col) are the two readings on the workbook. Raised as trace-precedents, trace-dependents and remove-arrows.

The same group carries the two dialogs that answer "why does this cell say that?". Evaluate Formula shows the active cell's formula with one part underlined, and each click on Evaluate replaces that part with what it is worth, until the formula has collapsed into the cell's answer; Step Back and Restart walk it again. A branch of an IF that is not taken is never evaluated, so it never reports an error the cell does not have, and a range such as A1:A9 stays where it is because a block is not a value.

Error Checking walks the cells on the sheet worth a second look, one at a time, with the selection following: every formula whose value is an error, said in a sentence rather than a code, and every formula that breaks the pattern of the ones above and below it. That second check is deliberately quiet. A formula is only called the odd one out when the cells above and below it both hold formulas, those two agree with each other once translated, and this one does not, which is the shape of a column filled down and then broken in the middle. Show Calculation Steps hands the cell straight to Evaluate Formula.

Both are engine functions first, so a save-time check or a report can use them with no browser:

import { evaluationSteps, checkSheet } from '@svgrid/enterprise'

const steps = evaluationSteps(parseFormula(text), (part) => wb.evaluateText(sheet, part))
const problems = checkSheet(sheet, {
  rowCount: () => wb.rowCount(sheet), colCount: () => wb.colCount(sheet),
  getRaw: (r, c) => wb.getRaw(sheet, r, c), getValue: (r, c) => wb.getValue(sheet, r, c),
})

Raised as evaluate-formula and error-checking.

The dialogs

Four of Excel's dialogs are the shell's own, so nothing on the ribbon or in the cell menu is a button that does nothing:

Dialog Opens from
Find and Replace Ctrl+F or Ctrl+H, Find & Select on the ribbon. Find Next, Find All, Replace, Replace All; match case, whole cell, look in values or formulas. Replace All is one undo.
Paste Special Ctrl+Shift+V, the Clipboard group's launcher, the last entry under the Paste arrow, the cell menu. All / Formulas / Values / Formats, Add / Subtract / Multiply / Divide, Skip blanks, Transpose. Works on what Ctrl+C took from the sheet.
Chart A double-click on a chart, or Insert > Setup while one is selected. Type (Column, Line, Area, Pie, Scatter), title, series in columns or rows, whether the first row and column are labels, stacking, a trendline over every series and one series on a secondary axis; Delete removes the chart.
Page Setup Page Layout > Print Titles, the Page Setup group's launcher. Orientation, paper, margins, scale, print area, rows to repeat at top, gridlines and headings; Print... applies and prints.
Protect Sheet Review > Protect Sheet. Excel's "allow all users of this worksheet to" list; OK protects with what is ticked.
Allow Edit Ranges Review > Allow Edit Ranges. Titled blocks that take an edit on a protected sheet; New over the selection, Modify, Delete, Protect Sheet...
Format Cells Ctrl+1, the launchers on the Font, Alignment and Number groups, the cell menu, and the end of Home > Cells > Format. Number (category, decimals, separator, the Accounting symbol, the Special type, custom code, live sample), Alignment (horizontal, vertical top/middle/bottom, wrap, indent), Font, Border presets, Fill, Protection. Opens on the active cell's format and applies only what was changed to the whole selection, as one undo.
Insert Function the fx button. Search or pick a category, read the signature and what the function does; OK starts the cell on =NAME( with the caret inside.
Name Manager Formulas > Name Manager, Ctrl+F3. Every defined name with what it refers to and its value; edit, delete, add.
Goal Seek Data > Goal Seek. Set a formula cell to a value by changing one input; the status page shows the answer and OK keeps it as one undo.
Create Table Insert > Table, Ctrl+T, and again on a cell inside a table, which is how one is renamed, resized or restyled. The range (the selection, or the region around the active cell), the name its columns are read by, whether the first row is the header, a totals row, and the styles gallery. A totals row asked for is ADDED under the data, as Excel's Total Row is, with Total in the first column and =SUBTOTAL(109,[Last column]) in the last; unticked, it goes. A structured reference is a range to SUBTOTAL, so a totals row's formulas skip the rows a filter folds. Insert > Table Styles opens the same dialog on the table the cursor is in.
Evaluate Formula Formulas > Evaluate Formula. The active cell's formula with the next part underlined; Evaluate replaces it with its value, Step Back and Restart walk it again.
Error Checking Formulas > Error Checking. Every cell on the sheet that reports an error, and every formula that breaks its column's pattern, walked with Previous and Next; Show Calculation Steps opens Evaluate Formula on the cell.
Calculation Options Formulas > Calculation Options. Excel's Enable iterative calculation, with the maximum passes and the smallest change worth another one; OK recalculates, so a circular reference goes from #CYCLE! to its fixed point, or back. See Iterative calculation below.
Sort Data > Sort. A level per key, each a column (named from the header row when "My data has headers" is on; the guess looks across every column of the block, so a Name column over names still reads as a header when the Pay column beside it is text over numbers, as Excel guesses it), what to sort on and an order; Add Level and Delete Level; the block is the selection or the region around the active cell. Numbers sort before text, blanks go last, ties keep their order, formats and one-row merges ride with their rows, and it is one undo. A level can sort on the cell colour or the font colour instead of the value: the list offers the colours that column carries, and the one picked goes On Top or On Bottom while every other row keeps its order, which is Excel's model, because two colours are not greater or lesser than one another. Sort A to Z and Z to A beside it sort on the active cell's column.
Text to Columns Data > Text to Columns. The delimiter is guessed from the column, the preview shows the split, Finish writes it as one undo.
Remove Duplicates Data > Remove Duplicates. Tick the columns that decide a duplicate, say whether the first row is headers; the count goes to the status bar.
Data Validation Data > Data Validation. Settings (Allow, Data, the bounds or the source, Ignore blank, In-cell dropdown) and Error Alert (Style, Title, Message); OK puts one rule over the selection, Clear All removes it. See Data validation below.
Conditional formatting Home > Styles > Conditional Formatting: Greater Than..., Less Than..., Between..., Equal To..., Text that Contains..., Duplicate Values..., Top 10 Items..., Bottom 10 Items..., Above Average..., Below Average... and New Rule > Use a Formula... each open the small dialog (the value or values, or the formula, and the "with" style); Manage Rules... opens the Rules Manager. See Conditional formatting below.

Home > Cells > Insert and Delete are Excel's split buttons. The face takes the selection's axis: a whole column selected inserts or deletes columns, anything else rows, which is what Excel does with a cell selected. The arrow spells the axis out, so a column goes in from a cell without selecting the column first: Insert Sheet Rows, Insert Sheet Columns and Insert Sheet (Shift+F11); Delete Sheet Rows, Delete Sheet Columns and Delete Sheet, which asks the tab menu's question first when the sheet holds anything (raised as delete-sheet). The cell menu and the column and row header menus offer the same Insert and Delete for their axis, and Ctrl+Shift++ and Ctrl+- take a whole row or column selection.

Home > Cells > Format is Excel's menu: Row Height..., AutoFit Row Height, Column Width... and AutoFit Column Width for the rows and columns the selection spans (row-height, autofit-rows, column-width, autofit-columns; an AutoFit is one undo), Hide Rows, Hide Columns, Unhide Rows and Unhide Columns with their keys, Lock Cell under Protection (a toggle, lit while the active cell is locked, raised as toggle-lock), and Format Cells... at the end. A ribbon item of kind dropdown is that shape: the face opens the list and every entry raises its own action.

AutoFilter

Excel's Filter: Ctrl+Shift+L, Home > Editing > Filter or Data > Filter puts an arrow on every header cell of the current region (the block around the active cell, its first row the headers; a selected range is the region instead). An arrow drops Excel's menu: Sort A to Z and Z to A (Smallest to Largest on a column of numbers), Clear Filter From the column, Filter by Color when the column's cells carry more than one fill (its own or a conditional format's, No Fill included), Date Filters on a column of dates (Today, This Week, Last Month, This Quarter, Year to Date and the rest, or Equals, Before, After and Between with a typed date; weeks run Sunday to Saturday), Top 10 on a column of numbers (top or bottom, so many items or so many percent, counted over the whole column), Text Filters or Number Filters (equals, does not equal, begins with, contains, greater than, between and the rest, two of them joined with And or Or), a search box, (Select All) and the column's values with their counts, the blanks last. A search narrows the list and OK (or Enter in the box) applies what it shows, as Excel does; Alt+Down on a header cell drops the menu. OK folds the rows that fail; the arrow turns into a funnel, the row numbers of the region turn blue and the status bar reads "N of M records found". A folded row is the filter's, not a hidden row: Unhide leaves it, getState().hidden leaves it out, and Filter off (Ctrl+Shift+L again) shows it while a row hidden by hand stays hidden. The rows are worked out again after every change, so a formula that drops out of a Number Filter folds away and one that comes back returns, as Excel's Reapply would; a sort from the menu keeps the header row where it is. Conditions run through the grid's own Excel-filter compiler (@svgrid/grid/filtering), so the operators are the ones the grid's filter row has. The filter is per sheet, moves with an insert or delete, rides in getState() as autoFilter ({ range, filters }, a filter being values, condition, date, color or top) and reports { kind: 'filter' } on onChange. Raised as toggle-filter. hiddenRowsFor takes a fillAt reader for the colour filters and a today for the date periods; datePeriodBounds is the calendar on its own.

Merged cells

Excel's Merge & Center, on Home > Alignment as Excel's split button: the face merges the selection into one cell and centres it, the arrow opens Merge & Center, Merge Across (one merge per row), Merge Cells (no centring) and Unmerge Cells; the cell menu offers Merge & Center on a range and Unmerge Cells on a merge, and Merge & Center on a merged cell unmerges it. A merge keeps the top-left cell's value and drops the rest, so a merge over values asks first, with Excel's words; the clears, the centring and the merge are one undo. The grid draws a merge as one cell (its mergedCells): it is selected as one, the arrow keys step over it, a click anywhere on it lands on its top-left cell, which is what the formula bar shows and an edit writes, and the covered cells take no entry, fill or paste. A merge moves with an insert or delete (and grows when rows are inserted inside it), a one-row merge rides with its row through a sort, and a sort over a range a merge crosses is refused with Excel's sentence. Merges are per sheet, ride in getState() as merges ([minRow, minCol, maxRow, maxCol]) and report { kind: 'merges' } on onChange. Raised as merge-center, merge-across, merge-cells and unmerge-cells.

Conditional formatting

Excel's rules that colour cells by what they hold, on Home > Styles > Conditional Formatting. Highlight Cells Rules (Greater Than, Less Than, Between, Equal To, Text that Contains, Duplicate Values) and Top/Bottom Rules (Top 10 Items, Bottom 10 Items, Above Average, Below Average) open the small dialog: the value or values (a literal or a formula, so =$B$1 follows B1) and the "with" style, Excel's Light Red Fill with Dark Red Text and the rest. Data Bar, the Green - Yellow - Red and Green - White Color Scales and the Icon Set (3 Arrows) apply to the selection at once; a data bar measures from zero, as Excel's do, so 10, 20 and 40 draw a quarter, a half and the full width. Clear Rules from Selected Cells cuts the selection out of every rule it touches; Clear Rules from Entire Sheet empties the sheet's rules. Manage Rules... is the Rules Manager: every rule in priority order with what it does, a sample and where it applies, Edit Rule (for the rules the dialog can edit), Delete Rule, Move Up, Move Down and Stop If True, for this worksheet or the current selection; OK applies the lot as one undo.

Rules are evaluated over the COMPUTED values, so a Greater Than on a column of formulas follows their results and a cell drops out when an input changes. Priority is the Rules Manager's order (a new rule goes first, as in Excel): the first rule that decides a property (the fill, the text colour, the bar, the icon) keeps it, and a rule with Stop If True stops the rules below it for the cells it matches. A rule's fill and text colour paint over the cell's own format, as they do in Excel. The rules are per sheet, move with an insert or delete, ride in getState() as conditionalFormats and report { kind: 'conditional-formats' } on onChange.

New Rule > Use a Formula... is Excel's "format values where this formula is true": the formula is written for the top-left cell of the selection and moves with each cell as a copied formula would, so =$B2>100 on A2:A9 reads each row's B, and TRUE (or a number other than 0) formats. The rule is { kind: 'formula', formula, style }. A data bar over a range with negatives puts its axis where zero falls between the minimum and the maximum, and the bars grow away from it: right for a positive value, left and in red (or the rule's negativeColor) for a negative one. evaluateCf, ruleStats, removeCf, shiftCf, describeCf and the rule types are exported from @svgrid/enterprise/sheet; the grid's own value-driven conditional formatting is a different thing (rules by column over raw values) and the shell does not use it.

Freeze Panes lives on the View tab, Excel's dropdown: Freeze Panes (at the active cell), Freeze Top Row, Freeze First Column, Unfreeze Panes (freeze-panes, freeze-top-row, freeze-first-column, unfreeze-panes). Beside it, View > Show has Excel's three toggles: Gridlines, Formula Bar and Headings (the row numbers and column letters), raised as toggle-gridlines, toggle-formula-bar and toggle-headings. They are view settings of the component, not part of the document, so getState() leaves them out.

Data validation

Excel's rules over cells: Data > Data Validation opens on the rule at the active cell and puts one over the selection. Allow is Any value, Whole number, Decimal, List, Date, Text length or Custom; the bounds (between, not between, equal to, greater than and the rest) are literals or formulas, so =$B$1 as a maximum follows B1 and a relative =A2 on G2:G17 reads the same row's A, as in Excel; a List is a comma list or a range or a name starting with =; a Custom formula is written for the top-left cell of the selection and moves with each cell, TRUE (or a number other than 0) allowing the entry. A typed entry that breaks the rule never reaches the workbook: the alert box shows the rule's title and message (or Excel's own words) with Retry, which reopens the editor with the entry, and Cancel; a Warning rule asks "Continue?" and Yes lets the entry through. The formula bar is checked the same way. Only typed entries are checked, as in Excel: paste, fill, Ctrl+D and the commands write what they are given.

A List rule with In-cell dropdown draws an arrow on the active cell; the arrow, or Alt+Down, drops the list (arrows and Enter pick, Escape leaves the cell alone), and a pick is one undo. A typed entry is matched against the list without regard to case.

The dialog's Input Message tab gives a rule a title and a message that show in a small box under the cell while it is selected, as Excel's do; the rule carries it as input. The arrow beside Data Validation has Circle Invalid Data, which draws Excel's red oval on every cell under a rule whose current contents break it (pasted, filled or written before the rule was), and Clear Validation Circles. The circles are worked out again on every repaint while they are on, so a corrected cell loses its circle at once; switching sheets turns them off. invalidCells is the same check for a shell of your own. Raised as circle-invalid and clear-circles.

The rules are per sheet, move with an insert or delete (and drop when their cells go), ride in getState() as validation and report { kind: 'validation' } on onChange. ruleAt, checkEntry, listChoices, removeValidation and shiftValidation are exported from @svgrid/enterprise/sheet for a shell of your own, with wb.evaluateText and wb.evaluateRange evaluating a bound or a list source in the sheet without storing it. Raised as data-validation and open-list.

Objects: charts and pictures

Insert > Chart charts the selected block, or the region of data around a single selected cell, as Excel starts one: the first row and column are read as the labels when they look like labels, each column of the block is a series, and the chart is anchored just under the block. It reads the range, not a copy of the numbers, so editing a cell redraws it. Insert > Picture puts an image on the sheet from a file, carried in the document as a data URL. A file the .xlsx cannot hold as it is (an SVG, say: Excel keeps only a few raster types in xl/media) is drawn to a PNG on the way in, so what is on the sheet is what the file will carry; a picture whose source is a web address is left out of the file, and Save As says so in the status bar.

=IMAGE(source, [alt]) is the other kind of picture, and the difference is the point: an IMAGE cell IS the picture rather than floating over one. It sorts with its row, filters with it, copies as a formula and moves when the cells move, with nothing to keep in step, which is what a catalogue with a thumbnail column wants. The source is a web address or a data: URL; anything else stays text rather than becoming a broken image. The second argument is the alt text, worked out like any other argument, so a screen reader is told what the picture is. The function's own value is the source, so a cell that reads it gets an address rather than a picture it cannot use, and the file stores it as _xlfn.IMAGE, which is where Excel keeps it.

An object floats over the cells rather than living in them: drag it to move, drag its corner to resize, press Delete to remove it, and double-click a chart (or Insert > Setup) to open the Chart dialog, where a trendline (linear or a three-point moving average) goes over every series and one series can be moved to a secondary axis on the right, which is what makes a revenue-and-margin chart readable when the two are orders of magnitude apart. Each change is one undo. It hangs from a cell and an offset inside it, the way Excel's does, so inserting a row above moves it, deleting that row takes it with it, and resizing a column under it moves it without reshaping it.

Objects are per sheet, ride in getState() as objects and report { kind: 'objects' } on onChange. The chart's own drawing is the free <SvChart> from @svgrid/grid, so its types, palette and tooltips are the grid's. Raised as insert-chart, insert-picture, chart-setup and delete-object, so an application can put its own chart builder in their place.

Objects print with the sheet. File > Print hangs each one from its anchor cell with the offset and size it has on the page, and the default print area grows down and across to hold a chart anchored below the numbers, which is where a chart usually is. A named print area is honoured exactly as it stands, so an object outside it is left out.

Objects ride in the .xlsx both ways, as Excel's own drawing part. A picture's bytes go into xl/media and a chart becomes a chart part of its own, both anchored to their cell; a file being opened gives its pictures and charts back the same way. A chart part carries the REFERENCES its series read rather than a copy of the numbers, so Excel redraws it from the cells beside it instead of from a snapshot that can drift. The one thing left behind is a picture whose source is a URL rather than a data: URL: its bytes are somewhere else, and a file with a broken image in it is worse than one without the image.

Two ways to put a link in a cell, both Excel's.

Insert > Link, or Ctrl+K, puts a link ON the cell: the address, the text to display, and a ScreenTip. The cell keeps whatever it says and the link is kept beside it, so editing the text keeps the link, a format change keeps it, and clearing the cell takes it away. Insert > Remove takes the links off the selection. The cell menu has Excel's entries too: Link... on a plain cell; Edit Link..., Open Link and Remove Link on one that carries a link. Each is one undo.

=HYPERLINK(link, [friendly]) puts one in a formula, showing the friendly name. A cell holding that formula is clickable too, and its target is whatever the first argument works out to, so a link built from its neighbours ("…/issues/" & A2) goes where it says.

A target that reads like an address moves the selection instead of leaving the page: Sheet2!B4, B4, or a defined name, switching sheets when it has to. Anything with a scheme, or a bare www., opens in a new tab. A single click follows the link and a drag from the same cell selects, which is Excel's rule.

Only the schemes a link can sensibly mean are followed: http, https, mailto, tel, sms, ftp and ftps. A target carrying any other one, javascript: among them, is not a link at all: the cell keeps its text, nothing is clickable, the target is never written into a saved file, and one found in a file being opened is dropped on the way in rather than carried into the document. A link is data, arriving from a colleague, a delta stream or an opened file, so what may be followed is named rather than assumed. Insert > Link refuses such an address in the dialog and names the ones a cell does open, and clicking a cell that already carries one says the same thing in the status bar rather than pretending the address is empty or somewhere on this workbook.

Links are per sheet, ride in getState() as links, report { kind: 'links' } on onChange, and move with an insert or a delete. They go into the .xlsx as real hyperlinks, an external one as a relationship with TargetMode="External" and an internal one as a location, tooltips included, and a file opened back carries both. Raised as insert-link and remove-link.

Iterative calculation

A circular reference is normally an error, and the shell says so: every cell in the loop shows #CYCLE! while the rest of the sheet keeps working. Some models are circular on purpose, because the answer is a fixed point: a bonus that is a tenth of the profit the bonus is taken out of, interest charged on the balance the interest is part of.

Formulas > Calculation Options is Excel's switch for those, raised from File > Options because it is the one setting there that changes what a formula is worth. It takes the maximum number of passes over the loop, 100 by default, and the smallest change worth another pass, 0.001 by default. With it on, the loop runs from the values it last had until the numbers stop moving or the passes run out, and every cell in it holds a number instead of an error.

It is workbook-wide rather than per sheet, so wb.iteration reads it and wb.setIteration({ enabled: true }) sets it from code, recalculating as it goes. It rides in getState() beside the defined names and the tables, and it goes into the .xlsx as calcPr, which is where Excel keeps it, so a file saved with iteration on opens with it on. Raised as calc-options.

PivotTable from a range

Insert > PivotTable summarises the selected block, or the region around a single selected cell, on the same pivot engine the grid uses for its own pivot mode. The dialog takes the source block (its first row the field names), where the result goes, and which field is a row, a column or a measure, with Sum, Average, Count, Distinct count, Min and Max to summarise by.

What the sheet keeps is the definition; what it writes is cells. A pivot's result is an ordinary block, so it can be formatted, charted, filtered, printed and saved to an .xlsx like anything else you typed. Insert > Refresh rebuilds it from the source and clears whatever the last one wrote, which is what a pivot over live cells owes the user. With the cursor inside a written block, Insert > PivotTable opens the dialog on that pivot instead of making another, so moving a field or changing an aggregation is two clicks. Each write is one undo, cells and definition together.

A field placed in Filter narrows the whole pivot to one of its values. The filter is written above the block, a line per filter with the field and the value, so a reader sees what the numbers are of before reading one; the dialog offers the values that field carries, and typing another value straight into the cell (or (All) to clear it) narrows the pivot at once, since the cell IS the control. The filter applies before anything is grouped, so the totals, the subtotals and Show Details all see the same rows.

Insert > Show Details is Excel's drill-down: the source rows behind the number in the cell the cursor is in, written to a sheet of their own, with the field names across the top and the first row frozen. A cell in a subtotal line opens its whole group, one in the grand total column opens the whole line, and the grand total opens every row, which is what each of those numbers stands for. The sheet is an ordinary one, so deleting it is how it is dismissed. pivotDrill is the same answer as data.

Pivots are per sheet, ride in getState() as pivots and report { kind: 'pivots' } on onChange. The definition moves with an insert or a delete, and is dropped when its source or its target cell is deleted. Raised as insert-pivot, refresh-pivot and pivot-details.

With one measure the block is Excel's compact form: the column values carry the corner label, the row labels run down the first column indented by level, and the grand total row and column can be turned off in the dialog. With two or more measures each column value gets one column per measure, under a header row naming them.

Sparklines

Excel's smallest chart, and the one that is not an object: a sparkline IS the cell. Insert > Sparklines offers Line, Column and Win/Loss, each opening the Create Sparklines dialog on the selected block with the data range filled in and the location the column just past it. One sparkline is drawn per row of the data (per column, when the location is a row), read from the range rather than a copy of it, so editing a number redraws the cell.

They are kept per GROUP, the way Excel keeps them: a data range, a location range of the same shape and the settings they share. So

Each of those is one undo. A group moves with an insert or a delete, and goes when the cells it is drawn in, or the cells it reads, are deleted. Sparklines are per sheet, ride in getState() as sparklines and report { kind: 'sparklines' } on onChange. The drawing is the free <SvSparkline> from @svgrid/grid. Raised as sparkline-line, sparkline-column, sparkline-winloss, sparkline-setup and clear-sparklines.

A sparkline is behind whatever the cell shows, so a label typed over one still reads, and the pointer goes through it: the cell is selected, dragged and edited as a cell. Sparklines ride in the .xlsx as Excel's own sparkline groups, both ways, in the worksheet's extension list where Excel keeps them, and they are drawn on the printed page too, in the cells they belong to.

<script>
  const doc = createSheetDocument({ workbook: wb })
  doc.get('Traffic').sparklines = [{
    id: sparklineId(),
    data: [1, 1, 4, 12],        // B2:M5, one row per channel
    location: [1, 13, 4, 13],   // N2:N5, the cells they are drawn in
    type: 'line',
    markers: true,
  }]
</script>

Comments

Excel's notes and its threaded comments: a text on a cell, marked by a red corner and read by hovering the cell, and under it the replies, each with its author and time. Review > New Comment, Shift+F2 and the cell menu's New Comment open the box beside the active cell; for a new comment it is the note editor, and Ctrl+Enter or Save closes it, as does Escape or a click elsewhere, keeping what was typed, as Excel keeps a note. On a cell that has one, the box is the thread: the first entry with Edit and Delete, the replies with theirs, a reply box (Ctrl+Enter posts) and Resolve in the head, which greys the thread in the list and hides the reply box until Reopen. Delete (on Review, in the box, in the cell menu) removes the whole thread; Previous and Next walk the sheet's comments row by row and wrap; Show All Comments opens a strip under the formula bar listing every comment with its author, each a jump to its cell, which is the honest form over a sheet that only paints the rows in view. Every change is one undo.

commentAuthor is who signs: with it set, a new comment and every reply carry that name and the time, the way Excel signs a threaded comment with the signed-in user; without it a new comment is a plain note and a reply carries its time alone. Comments are per sheet, move with an insert or delete, ride in getState() as comments (r4 -> B -> a note's text, or { text, author, at, replies, resolved } for a thread; every document saved before threads existed reads as it did) and report { kind: 'comments' } on onChange. Save As writes a note as Excel's legacy note and a thread as its threaded comment with the persons part, and Open reads both. Raised as new-comment, edit-comment, delete-comment, prev-comment, next-comment and toggle-comments.

Protection

Excel's model, without the password. Every cell is locked to begin with; Format Cells > Protection (or Format > Lock Cell) unlocks the ones that may change; Review > Protect Sheet turns the flags on. From then on a locked cell refuses to change wherever the change comes from: typing, F2, Delete, the formula bar, paste, a fill, Ctrl+D, Find and Replace (which skips it), a sort of the region, and the formats (the Font, Alignment and Number buttons grey out while the selection holds a locked cell, and Ctrl+B declines). Insert and Delete grey out for the whole sheet, so do Row Height, Column Width, AutoFit, Hide and Unhide, and the resize handles go away. The refusal is a sentence in the status bar, Excel's own: "The cell you're trying to change is on a protected sheet. To make a change, unprotect the sheet." Unlocked cells take every edit as before.

What is drawn OVER the cells is covered too: a chart or a picture cannot be moved, resized, retyped or deleted, and the sparklines of a protected sheet cannot be changed or cleared. The dialogs that would write them do not open on a protected sheet, and the refusal sits on the write itself, so every way in (the ribbon, a dialog, the keyboard, a drag) says the same thing.

Protect Sheet opens Excel's dialog with its "allow all users of this worksheet to" list: format cells, format columns, format rows, insert columns, insert rows, delete columns, delete rows, sort, use AutoFilter. What is ticked stays open once the sheet is protected: with Format cells the Font, Alignment and Number buttons, Format Cells, merges and Lock Cell work on every cell; with Format rows or columns Row Height, Column Width, AutoFit, Hide and Unhide and the resize handles come back for that axis; the insert and delete entries open Home > Cells > Insert and Delete for their axis; Sort allows a sort of a block that holds no locked cell, as Excel's does (a block with one is refused whatever the tick); Use AutoFilter allows the arrows on and off and their menus. The list is per sheet and kept, so Unprotect and Protect again open on the same ticks.

Review > Allow Edit Ranges is Excel's Allow Users to Edit Ranges: a list of titled blocks (B2:B10, or several areas with commas) that take an edit on a protected sheet whether their cells are locked or not. New adds one over the selection, Modify edits it in place, Delete removes it, and Protect Sheet... goes straight to that dialog. A range moves with an insert or delete and goes with the rows it spans.

Protect Sheet and Unprotect Sheet share one slot on the Review tab (protect-sheet, unprotect-sheet, and allow-edit-ranges); each is one undo, so Ctrl+Z after Unprotect protects again, and after Protect puts the old allow list back. The flag is per sheet and rides in getState() as protected, the list and the ranges as protection: { allow, ranges } beside it (absent in an older document, which reads as nothing allowed and no ranges), the unlocked cells as locked: false in their format entries; onChange reports { kind: 'protection' }. Save As writes the flag, the list and the ranges as the file's sheetProtection and protectedRanges, and Open reads Excel's. A command written against the grid asks cmd.canEdit(r, c) before it writes and gets the same answer the editor does.

There is no password, on the sheet or on a range: the protection is against mistakes, not against the person at the keyboard, who can unprotect with one click.

Data > Sort A to Z and Z to A sort the current region (or the selection) by the active cell's column: numbers before text, blanks last, a header row of text over numbers left in place, values and formats moving together, one undo.

Every one of them raises its onAction first. A handler that returns true has taken the action over and the shell's dialog stays closed, so an application can put its own Format Cells in place of this one:

<SvSheet workbook={wb} onAction={(action, cmd) => {
  if (action === 'format-cells') { openMyDialog(cmd); return true }
}} />

Grouping rows and columns

Data > Group folds a block of rows or columns away behind a button, the way Excel's outline does. Group and Ungroup act on the selection, grouping an already grouped run nests it, and the numbered buttons at the corner of the bar show the whole sheet at one depth. Auto Outline groups the runs between the totals the sheet already has; Clear Outline removes the lot.

The summary line is the one the button sits on, and it is the line AFTER the detail on both axes: the total row under its rows, the total column to the right of its columns. That is Excel's summaryBelow and summaryRight, and both round-trip through the .xlsx as outlineLevel and collapsed per line.

A line a collapsed group folds is not a line you hid. The two are kept apart in the document on purpose: if folding recorded a hidden row, the row would stay invisible after its group reopened and there would be no button left to bring it back. Unhide leaves a folded row to its group, the way it already leaves a filtered row to its filter.

Cells drawn as a control

Insert > Controls draws the selected cells as a checkbox or a button instead of as text, and SvSheet's model has a radio group beside them.

A control is a rendering and never a second source of truth. A checkbox is ticked because its cell reads TRUE, not because the checkbox remembers being clicked, so everything else keeps working over it: =COUNTIF(A1:A9, TRUE) counts the ticks, undo undoes one, a paste sets one, and the file carries an ordinary boolean. Excel's own cell checkbox works this way too, which is why the default pair is TRUE and FALSE. A sheet that wants Yes and No says so on the region.

A button reports its press through onCellAction:

<SvSheet {data}
  onCellAction={(event) => {
    // `event.action` is the region's own action name, so one column of
    // buttons can be told from another.
    if (event.action === 'approve') approve(event.row)
  }} />

Clear Control puts the cells back to ordinary ones and leaves their values where they are.

A tab that is a table, not cells

A workbook tab can be a bound table rather than a cell grid. Name it in gridSheets with its fields and its records and it renders as a <SvGrid>, with its own headers, sorting, filtering and inline editing, in the same workbook as the cell sheets beside it.

<SvSheet
  data={[{ name: 'Orders', cells: [] }, { name: 'Summary', cells: [] }]}
  gridSheets={{
    Orders: {
      fields: [
        { field: 'item', label: 'Item' },
        { field: 'qty', label: 'Qty', type: 'number' },
        { field: 'price', label: 'Price', type: 'number' },
      ],
      rows: orders,
      editable: true,
    },
  }} />

The records are the truth, and after every change they are projected into the workbook's cells, header row included. That is what makes a bound tab more than an embedded widget: on the Summary tab,

=SUM(Orders!B2:B99)
=VLOOKUP("Gadget", Orders!A2:C99, 2, FALSE)

both work, because the formula engine, the dependency graph, the file writers and the printer are never told that the Orders tab is different. The projection goes through the same write an ordinary edit uses, which ignores a write that changes nothing, so editing one field of one record rewrites one cell and recalculates only what read it.

What a bound tab deliberately does not do is hold formulas of its own. Its cells are a rendering of its records, so anything typed into them would be overwritten by the next projection. A sheet that needs formulas beside the data is a cell sheet reading the bound one across, which is what the example above is.

Localisation

Every string the shell shows is English by default and lives in one flat map, SheetMessages, the way the grid's GridMessages does. Pass any subset as localization.text and unset keys stay English; locale is the BCP-47 tag the status bar's Sum, Average and Count are printed with, and it reaches the grid underneath for its own matching and formatting.

<SvSheet {data}
  localization={{
    locale: 'de-DE',
    text: {
      'ribbon.tab.home': 'Start',
      'ribbon.bold.title': 'Fett',
      statusReady: 'Bereit',
      statusRecordsFound: '{shown} von {total} Datensätzen gefunden',
      'formatCells.title': 'Zellen formatieren',
      ok: 'OK', cancel: 'Abbrechen',
    },
  }} />

Three families of key:

A sentence with {placeholders} (statusRecordsFound, deleteSheetMessage, linesToggled, textToColumns.status) lets a translator choose the word order; formatMessage fills them. defaultSheetMessages is exported, so the full English map is there to copy from, and resolveSheetMessages gives the merged map for an app that wants to read it.

What stays English: the function names and error values (SUM, #VALUE!), which Excel keeps in every locale too, the rule descriptions the Conditional Formatting Rules Manager lists, and the font names.

How numbers and formulas are spelled

locale also decides how a number and a formula are WRITTEN. Under de-DE a cell shows 1.234,5, the formula bar shows =ROUND(A1/3; 2), and 1,5 typed into a cell is the number rather than text. The marks come from Intl, so a locale nobody planned for still comes out right, and Excel's own rule decides the separator: wherever the decimal mark is a comma the argument separator is a semicolon, since the comma is busy.

The document is unaffected, and that is the point. A formula is stored as =SUM(1.5, A1) whatever the locale, so a file written in Berlin opens in Boston and getState() means the same thing on every machine. The culture is put on when a formula is shown for editing and taken off when one is typed. A , typed where the separator is ; is taken as a separator too, because a comma with no digit after it cannot be a decimal mark, so an English keyboard habit is not punished.

A number FORMAT string stays invariant as well: #,##0.00 is the pattern in every locale, and it is the rendering of that pattern that follows the culture.

Override it where the locale is not the whole story:

<!-- German strings, but the dot for decimals this team is used to. -->
<SvSheet {data} localization={{ locale: 'de-DE', culture: { decimal: '.' } }} />

<!-- German strings, invariant spelling: for a team sharing files with
     English-speaking colleagues. -->
<SvSheet {data} localization={{ locale: 'de-DE', culture: false }} />

Two people on one sheet

createDeltaStream turns the document's changes into small messages, and applySheetDelta applies one that arrived from somewhere else. Put a socket between the two and the sheet is collaborative.

<script>
  import { createSheetDocument, createDeltaStream } from '@svgrid/enterprise'

  const doc = createSheetDocument({ sheets })
  const socket = new WebSocket(url)
  const stream = createDeltaStream(doc, {
    onDelta: (delta) => socket.send(JSON.stringify(delta)),
  })
  socket.onmessage = (event) => stream.apply(JSON.parse(event.data))
  // A new participant asks for everything once:
  socket.onopen = () => stream.resync()
</script>

<SvSheet document={doc} height="100%" />

Five kinds of delta go out, and each is as small as it can be:

The shell follows its document, so a delta applied from outside repaints the sheet without anything else being called. refresh() remains for a write made straight to the workbook, which the document never hears about.

Conflicts are last writer wins, per cell. A delta carries no version and no transform: two people typing in the same cell end on whichever message arrived last, and both converge on it. Two people typing in different cells never conflict, which is the case that actually happens. Anything stronger, operational transform or a CRDT, is a different piece of work and is not pretended at here. Applying is idempotent for cells and state and is not for structure, so a transport that can deliver twice has to deduplicate.

Presence: where everyone is

The deltas carry what people TYPE. Presence carries where they ARE, which is what stops two people typing into the same cell.

presence is a prop, a list of who else is on this workbook, and the shell draws each person on the active sheet as a thin coloured box around their selection with their name on their cursor. onPresence fires whenever this user's own selection moves, with the sheet, the rectangle and the active cell, which is what an application broadcasts.

<SvSheet
  document={doc}
  presence={others}
  onPresence={(me) => stream.sendPresence({ id: myId, name: myName, ...me })}
/>

The fifth delta kind, presence, rides the same wire so an application needs only one: sendPresence sends it, and it arrives at the receiving stream's onPresence rather than at the document. That is deliberate. Presence is never part of the document: not in getState(), not in the .xlsx, and applySheetDelta ignores it. A cursor belongs to a session, and a file that remembered where someone's cursor was last week would be remembering nothing worth keeping.

A person with no colour is given a stable one from their id, so a set of peers is never all one colour, and anyone who has not been heard from for fifteen seconds is dropped, since a closed tab says no goodbye. A cursor moves on every arrow key, so throttle onPresence if the transport charges by the message; the shell does not, because it cannot know what the transport costs.

How big a sheet it holds

Measured rather than guessed, by pnpm bench over the built engine (the cases are in tools/bench/sheet-cases.mjs, the numbers and the rig in Benchmarks). On a CI-class Linux container:

Sheet Opens in Retains
1,000 rows, a formula each 8 ms 1.6 MB
10,000 rows, a formula each 91 ms 16.5 MB
50,000 rows, a formula each 547 ms 84.2 MB

A keystroke costs one evaluation rather than a sheet's worth: the dependency graph recomputes what read the cell, and nothing else. What makes an edit expensive is not the size of the sheet but how much of it one cell feeds. A single SUM over 10,000 cells is one evaluation that re-reads ten thousand cells (14 ms); a running-balance column, where every row reads the row above, is 10,000 evaluations for one keystroke, and that is the true cost of that shape.

CI gates the evaluation counts rather than the milliseconds, since a count is the same on every machine.

Right to left, touch and assistive tech

The shell carries no direction of its own: set dir="rtl" on the page or on any element above it and everything mirrors, the row gutter and the frozen columns to the right edge, the ribbon and the tab strip in reading order, an anchored chart hanging from its cell's right edge and growing leftwards. The arrow keys follow the reading order with it, so on a right-to-left ribbon ArrowLeft is the next tab.

The two bands that cannot fit a phone pan with a finger rather than clipping: the ribbon scrolls sideways over its groups and over its tab labels, and the cells pan in both axes. The formula bar keeps a long entry on one line, cut off at the right and scrolling with the caret, until the chevron expands it; a one-row box that wrapped would only hide the rest. A tap picks a cell, a second tap on the same cell opens its editor, and the formula bar is where the address and the formula are read and typed.

For assistive tech the shell is the bands it looks like: the ribbon is a toolbar whose tab strip is a tablist with one Tab stop, the arrows plus Home and End moving between tabs with focus following the selection; the sheet tabs are a second tablist with the same model; the formula bar is a labelled group whose Name Box reads the active cell as its value; the cells are the grid's own role="grid"; every dialog takes focus and gives it back to the cells on Escape. tests/e2e/sheet-a11y.spec.ts, sheet-rtl.spec.ts and mobile/sheet-touch.spec.ts hold the shell to all of this in a real browser, the first with an axe audit of every band.

One thing the shell does not decide: a font colour or a fill the DOCUMENT sets is used as it stands, on a dark theme as on a light one. A red on a dark canvas is as hard to read here as it is in Excel, and the answer is the same one: change the document's colours, not the app's.

What it does not do

The shell is the part of Excel a team's spreadsheet uses every day; these are the parts it leaves out, on purpose, so nothing on the ribbon is a button that does nothing.

More examples

A tab that is a table, and rows that fold away

A workbook tab that holds RECORDS rather than cells: the Orders tab renders as the data grid, with its own headers, sorting, filtering and inline editing, beside ordinary cell sheets. Its records are projected into the workbook's cells, header row included, so the Summary tab reads it with plain formulas: SUM, SUMIF, SUMPRODUCT and VLOOKUP all reach across, because the formula engine is never told the tab is different. Summary also shows Data > Group, with three regional blocks folded under their subtotals and the numbered level buttons at the corner of the outline bar. Edit a Qty on Orders and every figure follows.

Open the live example: A tab that is a table, and rows that fold away (Spreadsheet)

Engineering, statistics, and the locale you type in

Four function families the engine gained at once, worked through a real example of each: a bearing from ATAN2 and DEGREES, a register mask in binary and hex with BITAND, an orchard queried through Excel's criteria-block grammar with DSUM, DCOUNT, DAVERAGE and DGET, and NORM.DIST with its inverse, a confidence interval and the chi-squared and t tails. The picker switches how numbers and formulas are SPELLED: German shows 1.234,5 and =ROUND(A1/3; 2) while the document still stores 1234.5 and a comma, so the file opens anywhere. Column H is drawn as checkboxes whose ticks an ordinary COUNTIF counts.

Open the live example: Engineering, statistics, and the locale you type in (Spreadsheet)

Review workflow: comments + Protect Sheet

An expense sheet under review on the Excel shell. It opens protected: every cell is locked except the reviewer's Receipt and Status columns, so a keystroke on an amount is refused and the status bar says why. Three reviewer notes sit on the cells they are about (Excel's red corner): hover, Shift+F2, Review > Next, Show All Comments. Status is a list, the totals by status are SUMIFs, and protection, notes and rules all ride in the document.

Open the live example: Cell comments + Protect Sheet (Spreadsheet)

Order intake: data validation

An order form with Excel's Data Validation on it: Region and Product are lists (the arrow or Alt+Down drops them; Product reads the price list on a second sheet), Qty is a whole number from 1 to 500 with a Stop alert and Retry, Discount is a decimal up to 20% with a Warning you can keep, Ship by is a date on or after the order date through a relative bound (=A2 moves with the row). Unit price is an XLOOKUP, the total follows.

Open the live example: Order intake: data validation (Spreadsheet)

Regional scorecard: conditional formatting

Eight regions, three months, a target, and every colour on the page is a conditional formatting rule over computed values: Highlight Cells on attainment (under 90% red, 100% and over green), a Data Bar and a Top 3 on the quarter, a Green - Yellow - Red Color Scale across the months, an Icon Set on the trend. Edit a month and the arrow, the bar, the fill and the rank all move; Manage Rules lists the six in priority order.

Open the live example: Regional scorecard: conditional formatting (Spreadsheet)

Budget report: merged headers

A half-year budget report laid out with merged cells the way Excel lays one out: a title merged across the page, Q1 / Q2 / H1 group headers merged over their months, a Line corner merged down two rows, a notes paragraph merged into one wrapped block. A merge is one cell to the grid: click inside Q1 and the Name Box says B2, Merge & Center lights, the arrows step over it and its column letters are shaded. Unmerge and merge again from the ribbon; Ctrl+Z each step.

Open the live example: Budget report: merged headers (Spreadsheet)

Support ticket log: AutoFilter

Forty support tickets with Excel's Filter on the header row, opened already filtered to what is still open: the funnel on Status, blue row numbers, "N of 40 records found" in the status bar. The arrows drop Excel's menu: sort, Clear Filter, Text and Number Filters with two conditions, a search box, (Select All) and the values with counts. The rows are worked out again after every edit, so a ticket typed Closed folds away at once. Ctrl+Shift+L toggles it.

Open the live example: Support ticket log: AutoFilter (Spreadsheet)

Autosave: a document that survives a reload

A project tracker that keeps itself in localStorage: onChange fires once per tick with every kind of change, the demo debounces it and writes getState() as JSON, and the next visit rebuilds the document with createSheetDocument({ state }) and hands it to . Add a comment, bold a row, hide a column, type a task, reload: it is all back, rules and merged title and frozen rows included. Reset puts the shipped document back with setState().

Open the live example: Autosave: a document that survives a reload (Spreadsheet)

Paste from Excel: formats and formulas survive

Excel puts an HTML document on the clipboard beside the tab-separated text: formats in a style block keyed by class, formulas in x:fmla, raw numbers in x:num. The sheet reads it, so a pasted block arrives bold, filled, with its number formats, and with its formulas moved to where they landed; Google Sheets' data-sheets-formula flavour reads the same. Two buttons put exactly what Excel and Sheets put on the clipboard; click a cell and Ctrl+V.

Open the live example: Paste from Excel: formats and formulas survive (Spreadsheet)

Loan model: PMT, accounting formats and the File tab

A mortgage model built from the sheet's own financial functions: PMT for the payment, IPMT and PPMT for each period's split, NPER and RATE for the term and the rate that fit, SUMPRODUCT for the first year of interest. The money cells wear the Accounting format, Extra Payment carries a validation rule with an Input Message, and Circle Invalid Data rings what breaks it. The File tab is the point: Save As writes the whole model as an .xlsx Excel opens with its formulas, formats and rules, Open reads one back, Export CSV takes the active sheet.

Open the live example: Loan model: PMT, accounting formats and the File tab (Spreadsheet)

Charts and pictures on the sheet

Charts anchored over the cells the way Excel anchors one: each reads a range rather than a copy of the numbers, so typing into a cell redraws it. Insert > Chart charts the selected block and reads its first row and column as the labels, Insert > Picture puts an image on the sheet, and a double-click opens the Chart dialog for the type, the title, series in columns or rows and stacking. Drag an object to move it, its corner to resize, Delete to remove. It hangs from a cell, so inserting a row above moves it, and it rides in getState().

Open the live example: Charts and pictures on the sheet (Spreadsheet)

Sparklines: a chart inside the cell

Excel's smallest chart, and not an object: a sparkline IS the cell. One per row of a block of numbers, drawn from the range rather than a copy, so editing a number redraws it. Insert > Sparklines offers Line, Column and Win/Loss, with Edit for the group's ranges, kind and colours and Clear for the groups the selection touches; selecting a cell that holds one turns the kind buttons into a change to that group. They are kept per group the way Excel keeps them, ride in getState() and move with an insert or a delete.

Open the live example: Sparklines: a chart inside the cell (Spreadsheet)

PivotTable from a range

Excel's Insert > PivotTable over a block of cells, on the same pivot engine the grid uses for its own pivot mode. The sheet keeps the definition, the source block, where the result goes and which field is a row, a column or a measure; the result is plain cells written in one undo, so it can be formatted, charted, printed and saved to an .xlsx like any other block. Refresh rebuilds it from the source, opening the dialog from inside one edits it, and Show Details writes the source rows behind a cell to a sheet of their own.

Open the live example: PivotTable from a range (Spreadsheet)

Two people on one sheet

Two full spreadsheets over two separate documents, wired to each other by createDeltaStream: type in either and the other follows, and each window shows the other's cursor as a coloured box with a name on it. What crosses the wire is a delta rather than the document, and the log shows each one as it goes: a formula travels as its text so the other side works out its own answer, an insert travels as the edit so both rewrite their own formulas, a format travels as the one part of the one sheet that changed. Conflicts are last writer wins, per cell.

Open the live example: Two people on one sheet: the delta stream (Spreadsheet)

Evaluate Formula and Error Checking

The two auditing tools that answer why a cell says what it says, over a commission model with three planted faults. Evaluate Formula underlines one part of the formula and replaces it with its value on each click. Error Checking walks every cell that reports an error, with a sentence on what each one means, and the one row whose formula is not the column's formula. Show Calculation Steps takes one straight into the other.

Open the live example: Evaluate Formula and Error Checking (Spreadsheet)

Iterative calculation

A circular reference is normally an error, and every cell in the loop shows #CYCLE!. Two models here are circular on purpose: a bonus that is a share of the profit it is taken out of, and interest charged on the balance it is part of. Formulas > Calculation Options turns iteration on with its two limits, and both settle on their fixed point; turn it off and the cycle is an error again. The setting rides in getState() and goes into the .xlsx as calcPr.

Open the live example: Iterative calculation (Spreadsheet)

IMAGE: a picture inside the cell

Excel's IMAGE puts a picture IN a cell rather than floating one over it, so it sorts with its row, filters with it, copies as a formula and moves when the cells move. A product list whose thumbnail column reads the swatch beside it: sort by price and the pictures follow their rows. The source is a web address or a data URL, anything else stays text rather than becoming a broken image, and the second argument is the alt text a screen reader reads.

Open the live example: IMAGE: a picture inside the cell (Spreadsheet)

Excel's two ways of putting a link in a cell. Insert > Link (Ctrl+K) puts one on the cell, so editing the text keeps it and clearing the cell takes it away; the HYPERLINK function puts one in a formula with a friendly name. A target that reads like an address moves the selection instead of leaving the page, so a cell links to another sheet; anything with a scheme opens in a new tab. A single click follows, a drag selects. Links ride in getState(), move with an insert, and go into the .xlsx both ways.

Open the live example: Hyperlinks: Insert > Link and HYPERLINK (Spreadsheet)

See also

Live examples

  • Spreadsheet + Ribbon bar - The whole Excel surface as one component, <SvSheet workbook={wb} />: a six-tab ribbon (Home, Insert, Formulas, Data, Review, View), the Name Box and fx bar, sheet tabs and the Sum / Average / Count status bar. Format cells, merge them, comment on them, validate what goes in, colour them by rule, filter the region, protect the sheet; every button is the same call as its shortcut, so Ctrl+B and the Bold button cannot drift. A two-sheet P&L with the formats travelling in the document.
  • Sales report workbook - A three-sheet workbook on the Excel shell: Orders with XLOOKUP prices and IF discounts, a Products price list, and a Summary where every figure is live - COUNTIF / SUMIF by region, share of a named total, RANK, per-rep attainment with an IFS status, INDEX / MATCH for the best region. Edit an order and the whole page follows across sheets. Defined names in the Name Box; bands, currency and percent formats ship with the document.
  • What-if analysis: Goal Seek - A pricing model with three scenarios side by side and Excel's Goal Seek over it: Data -> Goal Seek, set the profit cell to 20,000 by changing the price, and the solver (secant with a bisection fallback) finds it. The sheet does not move until the status dialog says "found a solution" and you press OK; OK writes through the grid's command context, so it is one Ctrl+Z.
  • Data cleanup: Text to Columns, Remove Duplicates - A CRM export landed in column A, semicolon-separated, with the same people in it twice. Data -> Text to Columns opens a wizard that has already guessed the delimiter and previews the split; Data -> Remove Duplicates lets you tick the columns that decide identity, compares case-insensitively like Excel, and reports "3 duplicate values found and removed; 11 unique values remain". Each operation is one Ctrl+Z.
  • A tab that is a table, and rows that fold away - A workbook tab that holds RECORDS rather than cells: the Orders tab renders as the data grid, with its own headers, sorting, filtering and inline editing, beside ordinary cell sheets. Its records are projected into the workbook's cells, header row included, so the Summary tab reads it with plain formulas: SUM, SUMIF, SUMPRODUCT and VLOOKUP all reach across, because the formula engine is never told the tab is different. Summary also shows Data > Group, with three regional blocks folded under their subtotals and the numbered level buttons at the corner of the outline bar. Edit a Qty on Orders and every figure follows.
  • Engineering, statistics, and the locale you type in - Four function families the engine gained at once, worked through a real example of each: a bearing from ATAN2 and DEGREES, a register mask in binary and hex with BITAND, an orchard queried through Excel's criteria-block grammar with DSUM, DCOUNT, DAVERAGE and DGET, and NORM.DIST with its inverse, a confidence interval and the chi-squared and t tails. The picker switches how numbers and formulas are SPELLED: German shows 1.234,5 and =ROUND(A1/3; 2) while the document still stores 1234.5 and a comma, so the file opens anywhere. Column H is drawn as checkboxes whose ticks an ordinary COUNTIF counts.
  • Cell comments + Protect Sheet - An expense sheet under review on the Excel shell. It opens protected: every cell is locked except the reviewer's Receipt and Status columns, so a keystroke on an amount is refused and the status bar says why. Three reviewer notes sit on the cells they are about (Excel's red corner): hover, Shift+F2, Review > Next, Show All Comments. Status is a list, the totals by status are SUMIFs, and protection, notes and rules all ride in the document.
  • Order intake: data validation - An order form with Excel's Data Validation on it: Region and Product are lists (the arrow or Alt+Down drops them; Product reads the price list on a second sheet), Qty is a whole number from 1 to 500 with a Stop alert and Retry, Discount is a decimal up to 20% with a Warning you can keep, Ship by is a date on or after the order date through a relative bound (=A2 moves with the row). Unit price is an XLOOKUP, the total follows.
  • Regional scorecard: conditional formatting - Eight regions, three months, a target, and every colour on the page is a conditional formatting rule over computed values: Highlight Cells on attainment (under 90% red, 100% and over green), a Data Bar and a Top 3 on the quarter, a Green - Yellow - Red Color Scale across the months, an Icon Set on the trend. Edit a month and the arrow, the bar, the fill and the rank all move; Manage Rules lists the six in priority order.
  • Budget report: merged headers - A half-year budget report laid out with merged cells the way Excel lays one out: a title merged across the page, Q1 / Q2 / H1 group headers merged over their months, a Line corner merged down two rows, a notes paragraph merged into one wrapped block. A merge is one cell to the grid: click inside Q1 and the Name Box says B2, Merge & Center lights, the arrows step over it and its column letters are shaded. Unmerge and merge again from the ribbon; Ctrl+Z each step.
  • Support ticket log: AutoFilter - Forty support tickets with Excel's Filter on the header row, opened already filtered to what is still open: the funnel on Status, blue row numbers, "N of 40 records found" in the status bar. The arrows drop Excel's menu: sort, Clear Filter, Text and Number Filters with two conditions, a search box, (Select All) and the values with counts. The rows are worked out again after every edit, so a ticket typed Closed folds away at once. Ctrl+Shift+L toggles it.
  • Autosave: a document that survives a reload - A project tracker that keeps itself in localStorage: onChange fires once per tick with every kind of change, the demo debounces it and writes getState() as JSON, and the next visit rebuilds the document with createSheetDocument({ state }) and hands it to <SvSheet document>. Add a comment, bold a row, hide a column, type a task, reload: it is all back, rules and merged title and frozen rows included. Reset puts the shipped document back with setState().
  • Paste from Excel: formats and formulas survive - Excel puts an HTML document on the clipboard beside the tab-separated text: formats in a style block keyed by class, formulas in x:fmla, raw numbers in x:num. The sheet reads it, so a pasted block arrives bold, filled, with its number formats, and with its formulas moved to where they landed; Google Sheets' data-sheets-formula flavour reads the same. Two buttons put exactly what Excel and Sheets put on the clipboard; click a cell and Ctrl+V.
  • Loan model: PMT, accounting formats and the File tab - A mortgage model built from the sheet's own financial functions: PMT for the payment, IPMT and PPMT for each period's split, NPER and RATE for the term and rate that fit, SUMPRODUCT for the first year of interest. The money cells wear the Accounting format, Extra Payment carries a validation rule with an Input Message, and Circle Invalid Data rings what breaks it. The File tab is the point: Save As writes the whole model as an .xlsx Excel opens with its formulas, formats and rules, Save As ODS writes the same model for LibreOffice Calc and Save As XLS the Excel 97-2003 binary, Open reads any of them (or a .csv) back by their bytes rather than their name, and Export CSV takes the active sheet.
  • Charts and pictures on the sheet - Charts anchored over the cells the way Excel anchors one: each reads a range rather than a copy of the numbers, so typing into a cell redraws it. Insert > Chart charts the selected block and reads its first row and column as the labels, Insert > Picture puts an image on the sheet, and a double-click opens the Chart dialog for the type, the title, series in columns or rows and stacking. Drag an object to move it, its corner to resize, Delete to remove. It hangs from a cell, so inserting a row above moves it, and it rides in getState() and in the .xlsx as a real drawing part.
  • Sparklines: a chart inside the cell - Excel's smallest chart, and not an object: a sparkline IS the cell. One per row of a block of numbers, drawn from the range rather than a copy, so editing a number redraws it. Insert > Sparklines offers Line, Column and Win/Loss, with Edit for the group's ranges, kind and colours and Clear for the groups the selection touches; selecting a cell that holds one turns the kind buttons into a change to that group. They are kept per group the way Excel keeps them, ride in getState() and in the .xlsx where Excel keeps them, and move with an insert or a delete.
  • PivotTable from a range - Excel's Insert > PivotTable over a block of cells, on the same pivot engine the grid uses for its own pivot mode. The sheet keeps the definition - the source block, where the result goes, and which field is a row, a column or a measure - and writes the result as plain cells in one undo, so it can be formatted, charted, printed and saved to an .xlsx like any other block. Show Details writes the source rows behind a cell to a sheet of their own, and Refresh rebuilds it from the source; opening the dialog from inside one edits it.
  • Two people on one sheet: the delta stream - Two full spreadsheets over two separate documents, wired to each other by createDeltaStream: type in either and the other follows. What crosses the wire is a delta rather than the document, and the log shows each one as it goes - a formula travels as its text so the other side works out its own answer, an insert travels as the edit so both rewrite their own formulas, a format travels as the one part of the one sheet that changed. Presence rides the same wire and is never part of the document: each window draws the other's selection as a coloured box with a name on it. Conflicts are last writer wins, per cell. A real app puts a socket in the middle.
  • Evaluate Formula and Error Checking - The two auditing tools that answer why a cell says what it says. Evaluate Formula underlines one part of the formula and replaces it with its value on each click, until the formula is the answer. Error Checking walks every cell that reports an error, with a sentence on what each one means, and every formula that breaks the pattern of the ones above and below it. Both are engine functions too: evaluationSteps() and checkSheet() run with no browser.
  • Iterative calculation - A circular reference is normally an error, and every cell in the loop shows #CYCLE!. Some models are circular on purpose: a bonus that is a share of the profit it is taken out of, interest charged on the balance it is part of. Formulas > Calculation Options turns on iterative calculation, and the loop runs until it stops moving or the passes run out. The setting rides in getState() and goes into the .xlsx as calcPr, so the file opens the same way in Excel.
  • IMAGE: a picture inside the cell - Excel's IMAGE puts a picture IN a cell rather than floating one over it, so it sorts with its row, filters with it, copies as a formula and moves when the cells move. A product list whose thumbnail column reads the swatch beside it: sort by price and the pictures follow their rows. The source is a web address or a data URL, anything else stays text rather than becoming a broken image, and the second argument is the alt text a screen reader reads.
  • Hyperlinks: Insert > Link and HYPERLINK - Excel's two ways of putting a link in a cell. Insert > Link (Ctrl+K) puts one ON the cell, so editing the text keeps it and clearing the cell takes it away; the HYPERLINK function puts one in a formula with a friendly name. A target that reads like an address moves the selection instead of leaving the page, so a cell links to another sheet; anything with a scheme opens in a new tab. A single click follows, a drag selects. Links ride in getState(), move with an insert, and go into the .xlsx both ways.

Related articles