Workbook (multi-sheet)
Three sheets and one formula engine spanning them: Prices is a lookup table, Orders VLOOKUPs into it, Summary SUMs over Orders. Edit a unit price and the change travels two sheets, recomputing only what depended on it. Tabs switch, rename and reorder; Ctrl+PageUp/PageDown and Shift+F11 work from anywhere in the grid. (requires @svgrid/enterprise)
A live, editable Svelte 5 data grid example from the SvGrid gallery (Spreadsheet). See the SvGrid documentation for the full API.
About this example
A multi-sheet workbook on a Svelte 5 spreadsheet grid with one formula engine spanning the sheets: Prices is a lookup table, Orders VLOOKUPs into it and Summary SUMs over Orders, so editing a unit price travels two sheets and recomputes only what depended on it. Sheet tabs switch, rename and reorder; Ctrl+PageUp and Ctrl+PageDown move between sheets and Shift+F11 inserts one, from anywhere in the grid. Enterprise, in @svgrid/enterprise.
Three sheets and one formula engine spanning them.
Prices a lookup table Orders =VLOOKUP into Prices, so editing a price moves every order Summary =SUM over Orders, so it moves again
Edit a price on the first tab and watch the change travel two sheets. Nothing recomputes that did not need to: the dependency graph spans sheets, so a change propagates exactly as far as it has to.
Tabs click to switch, double-click or F2 to rename, drag to reorder, + to add. Ctrl+PageDown/Up next / previous sheet. No wrapping, as in Excel. Shift+F11 new sheet.
A cell past the written area reads as BLANK, not #REF!, so =SUM(B1:B100) over a short sheet is an ordinary thing to write.
Imports, features and API used
Imports: @svgrid/grid, @svgrid/enterprise
Frequently asked questions
How do formulas reach another sheet?
With the Sheet!A1 form, Prices!B2 or Prices!A2:B20 in a VLOOKUP. The workbook keeps one dependency graph across every sheet, so a change on Prices recalculates the cells on Orders and Summary that read it.
Can sheets be renamed?
Yes, double-click a tab or use its menu. References in formulas follow the rename, so =SUM(Orders!D2:D9) keeps working after Orders becomes Sales.
Which keys move between sheets?
Ctrl+PageUp and Ctrl+PageDown switch to the previous and next sheet, and Shift+F11 inserts a new one, all while the focus stays in the grid.
Related documentation
Related articles
- A Fill Handle (Drag to Fill) in SvGrid - Build a working spreadsheet-style fill handle on top of SvGrid's cell selection and editing - pointer tracking, range highlighting, series fill, and undo/redo integration all covered.
- Pivot Tables in Svelte - Summarize Data Without a Spreadsheet - Run a cross-tab pivot directly inside your Svelte app using @svgrid/enterprise createPivotModel - no Excel, no server-side aggregation, no stale exports.
- Spreadsheet-Style Cell Range Selection in SvGrid - How to enable drag-to-select cell ranges, read live selection state, and build a status-bar footer that sums and averages the selected values.
Source code (455-workbook.svelte)
<script lang="ts">
/**
* 455. Workbook - several sheets that read each other
* ----------------------------------------------------
* Three sheets and one formula engine spanning them.
*
* Prices a lookup table
* Orders =VLOOKUP into Prices, so editing a price moves every order
* Summary =SUM over Orders, so it moves again
*
* Edit a price on the first tab and watch the change travel two sheets.
* Nothing recomputes that did not need to: the dependency graph spans
* sheets, so a change propagates exactly as far as it has to.
*
* Tabs click to switch, double-click or F2 to rename, drag
* to reorder, + to add.
* Ctrl+PageDown/Up next / previous sheet. No wrapping, as in Excel.
* Shift+F11 new sheet.
*
* A cell past the written area reads as BLANK, not #REF!, so =SUM(B1:B100)
* over a short sheet is an ordinary thing to write.
*/
import { SvGrid, tableFeatures, renderSnippet, type GridColumns } from '@svgrid/grid'
import {
enableSheet, setWorkbook,
SvSheetTabs, SvFormulaBar,
createWorkbook, formatCellValue,
type SheetCellValue,
} from '@svgrid/enterprise'
enableSheet()
const LETTERS = ['A', 'B', 'C', 'D'] as const
const seed = () => [
{
name: 'Prices',
cells: [
['SKU', 'Unit'],
['SKU-100', '10'],
['SKU-110', '25'],
['SKU-120', '40'],
],
},
{
name: 'Orders',
cells: [
['SKU', 'Qty', 'Unit', 'Total'],
['SKU-100', '3', "=VLOOKUP(A2,Prices!A2:B4,2)", '=B2*C2'],
['SKU-120', '2', "=VLOOKUP(A3,Prices!A2:B4,2)", '=B3*C3'],
['SKU-110', '5', "=VLOOKUP(A4,Prices!A2:B4,2)", '=B4*C4'],
],
},
{
name: 'Summary',
cells: [
['Metric', 'Value'],
['Orders', '=COUNT(Orders!B2:B4)'],
['Units', '=SUM(Orders!B2:B4)'],
['Revenue', '=SUM(Orders!D2:D4)'],
['Average', '=ROUND(B4/B3,2)'],
],
},
]
let version = $state(0)
let active = $state({ rowIndex: 1, colIndex: 1 })
const wb = createWorkbook(seed(), { onRecalc: () => (version += 1) })
setWorkbook(wb, () => { version += 1; active = { rowIndex: 0, colIndex: 0 } })
type SheetRow = { id: string; index: number }
const rows = $derived.by<SheetRow[]>(() => {
void version
const n = Math.max(wb.rowCount(wb.active), 6)
return Array.from({ length: n }, (_, i) => ({ id: `r${i}`, index: i }))
})
const colCount = $derived.by(() => {
void version
return Math.max(wb.colCount(wb.active), 4)
})
function shown(r: number, c: number): { text: string; error: boolean } {
void version
const value: SheetCellValue = wb.getValue(wb.active, r, c)
const error = typeof value === 'object' && value !== null && 'error' in value
return { text: formatCellValue(value), error }
}
const activeRaw = $derived.by(() => {
void version
return wb.getRaw(wb.active, active.rowIndex, active.colIndex)
})
function commit(text: string, cell: { rowIndex: number; colIndex: number }) {
wb.setRaw(wb.active, cell.rowIndex, cell.colIndex, text)
version += 1
}
const features = tableFeatures({})
const columns = $derived<GridColumns<SheetRow>>(
Array.from({ length: colCount }, (_, c) => ({
id: LETTERS[c] ?? `col${c}`,
header: LETTERS[c] ?? String(c),
width: c === 0 ? 150 : 120,
editable: false,
cell: (cc: { row: { original: SheetRow } }) =>
renderSnippet(Cell, { r: cc.row.original.index, c }),
})),
)
</script>
{#snippet Cell(props: { r: number; c: number })}
{@const cell = shown(props.r, props.c)}
{@const isActive = active.rowIndex === props.r && active.colIndex === props.c}
{@const raw = wb.getRaw(wb.active, props.r, props.c)}
<button
type="button"
class="cell"
class:active={isActive}
class:error={cell.error}
class:formula={raw.startsWith('=')}
title={raw}
onclick={() => (active = { rowIndex: props.r, colIndex: props.c })}
>{cell.text}</button>
{/snippet}
<section class="wrap">
<SvFormulaBar
active={active}
value={activeRaw}
onCommit={commit}
onNavigate={(cell) => (active = cell)}
/>
<SvGrid
data={rows}
{columns}
{features}
selectionMode="cell"
enableCellSelection={true}
filterMode="none"
containerHeight={230}
/>
<!-- `version` goes back IN as well as out: a Workbook is a plain object,
so the strip has no reactive dependency on it and would not notice
Ctrl+PageUp/PageDown switching the sheet underneath it. -->
<SvSheetTabs workbook={wb} {version} onChange={() => (version += 1)} />
<p class="note">
Change a Unit price on <strong>Prices</strong>, then look at
<strong>Orders</strong> and <strong>Summary</strong>: the VLOOKUP and both
SUMs have already moved. Blue cells hold a formula; the bar shows the
source behind whichever one is selected.
</p>
</section>
<style>
.wrap { display: flex; flex-direction: column; gap: 8px; }
.cell {
display: block; width: 100%; height: 100%; text-align: inherit;
font: inherit; border: 0; background: transparent; color: inherit;
padding: 0 2px; cursor: pointer;
}
.cell.formula { color: var(--sg-accent, #4f46e5); }
.cell.error { color: var(--sg-danger, #dc2626); font-family: ui-monospace, Menlo, monospace; }
.cell.active { box-shadow: inset 0 0 0 2px var(--sg-accent, #6366f1); border-radius: 2px; }
.note { margin: 0; font-size: 13px; line-height: 1.6; color: var(--sg-muted, #64748b); }
</style>More Spreadsheet 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.
- Spreadsheet + formulas - Real formula engine inside the grid: cell refs (A1), ranges (A1:A10), SUM / AVG / IF / COUNTIF / ROUND, arithmetic, string concat, cycle detection.
- Per-cell custom borders (KPI) - Editable KPI scorecard. spreadsheetLayout paints spreadsheet-style per-edge custom borders via an absolute-positioned overlay (no border-collapse conflicts). Edit any quarter or target - the borders re-derive: green double = beat target, blue solid = hit, amber dotted = near miss, red dashed = bad miss; row champion gets a colored full frame.
- Cell merging (spreadsheet shell) - A real invoice rendered on an Excel-style shell: A / B / C / D / E column letters across the top, row numbers down the left. Brand band, bill-from / bill-to address blocks, meta block, line items, totals, notes, signatures - all assembled from MergeSpec + CellBorderSpec. Editable Qty / Rate / addresses / notes; totals recompute live.
- HyperFormula integration - Full HyperFormula engine wired into the grid as a peer-optional dep. Editable spreadsheet with A1-style cell refs, dozens of formulas across math (SUM / SUMIF), lookup (VLOOKUP / INDEX-MATCH), text (CONCAT / UPPER), date (TODAY / DATEDIF), logical (IF nests), financial (PMT / IRR / NPV), statistical (AVERAGE / MAX / RANK).