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

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>

View this example on GitHub

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).