Spreadsheet: getting started
The one component a spreadsheet needs, how it takes data, how a formula reaches another sheet, and the document underneath that a save and a reload go through. The other spreadsheet tutorials assume this one, and The spreadsheet shell is the reference behind all of them: every prop, every dialog, the file formats, the limits.
SvSheet is a spreadsheet, not a mode of the grid: it owns a workbook of
formulas, address-keyed formats, comments and rules, and draws it with
<SvGrid> underneath. It ships in @svgrid/enterprise on top of
@svgrid/grid; jszip is a peer you add only when a file is saved or
opened as .xlsx or .ods.
npm install @svgrid/grid @svgrid/enterprise
The examples on this page share one setup: a small budget as raw text, and the lookup that formats a rectangle of cells by row and column index.
<script lang="ts">
import { SvSheet, createWorkbook, createSheetDocument, sheetCellsFromRows } from '@svgrid/enterprise'
// Every cell is the text the user would have typed: a number as its
// digits, a formula starting with `=`, an empty string for nothing.
const budget: string[][] = [
['Line', 'Jan', 'Feb', 'Mar', 'Q1'],
['Rent', '2400', '2400', '2400', '=SUM(B2:D2)'],
['Payroll', '18500', '18500', '19200', '=SUM(B3:D3)'],
['Marketing', '3100', '4800', '2600', '=SUM(B4:D4)'],
['Travel', '900', '1350', '400', '=SUM(B5:D5)'],
['Total', '=SUM(B2:B5)', '=SUM(C2:C5)', '=SUM(D2:D5)', '=SUM(E2:E5)'],
]
// formats.set takes rectangles as [minRow, minCol, maxRow, maxCol],
// 0-based, and this lookup turns an index into the key the store uses.
const at = { rowIdAt: (i: number) => `r${i}`, columnIdAt: (i: number) => String.fromCharCode(65 + i) }
</script>
The minimum
No props at all is a working spreadsheet: fifty rows, twelve columns, the
ribbon, the formula bar, the sheet tabs and the status bar. Click a cell
and type a number, or =SUM(A1:A5), and the Excel keyboard is already
live: Ctrl+Arrow to the edge of a block, Ctrl+D and Ctrl+R to fill
down and right, Ctrl+; for today, Alt+= for AutoSum, Ctrl+Z for a
whole action rather than one cell.
<SvSheet />
Open the live example: Blank sheet - just type (Spreadsheet)
Seed it with data
data takes the sheets to start with, each a name and its cells as raw
text, row by row. A ragged row is fine; a missing cell reads as empty.
Numbers are digits, dates are yyyy-mm-dd, and a formula is the text
that starts with =, exactly as the user would have typed it - the
sheet works the values out.
<SvSheet data={[{ name: 'Budget', cells: budget }]} rows={12} columns={6} />
Change any month and the row's Q1 and the Total row recompute. What the
sheet keeps is the text, so getState() (below) hands back =SUM(B2:D2),
not 7200.
A formula that reads another sheet
A workbook with more than one sheet is createWorkbook, which takes the
same list data does and gives you the object the formulas run through.
A reference qualifies the sheet the way Excel writes it: Prices!B2, or
'Price list'!B2 when the name has a space. $ pins a row or a column
when the formula is filled down.
<script lang="ts">
const wb = createWorkbook([
{ name: 'Orders', cells: [
['Item', 'Qty', 'Unit', 'Amount'],
['Desk', '4', '=XLOOKUP(A2,Prices!$A$2:$A$4,Prices!$B$2:$B$4)', '=B2*C2'],
['Chair', '12', '=XLOOKUP(A3,Prices!$A$2:$A$4,Prices!$B$2:$B$4)', '=B3*C3'],
['Lamp', '6', '=XLOOKUP(A4,Prices!$A$2:$A$4,Prices!$B$2:$B$4)', '=B4*C4'],
['Total', '', '', '=SUM(D2:D4)'],
] },
{ name: 'Prices', cells: [
['Item', 'Unit price'],
['Desk', '320'],
['Chair', '85'],
['Lamp', '40'],
] },
])
</script>
<SvSheet workbook={wb} rows={10} columns={6} />
Switch to the Prices tab, change a price, and switch back: the amounts
followed. Ctrl+PageUp and Ctrl+PageDown move between sheets from the
keyboard, and Shift+F11 adds one. Workbooks is
the page on sheets that read each other; Formulas is the
page on what a formula can say.
A look it opens with
A sheet a person types into can be formatted by hand from the ribbon. A
sheet you ship should open formatted, or it is a grid of raw numbers
until someone tidies it. formats takes cell formats keyed by address
('Orders!F2' reaches another sheet), and columnWidths widths keyed by
letter:
<SvSheet
data={[{ name: 'Budget', cells: budget }]}
rows={12}
columns={6}
columnWidths={{ A: 130 }}
formats={{
A1: { bold: true, fill: '#e2e8f0' }, B1: { bold: true, fill: '#e2e8f0' }, C1: { bold: true, fill: '#e2e8f0' }, D1: { bold: true, fill: '#e2e8f0' }, E1: { bold: true, fill: '#e2e8f0' },
B2: { numFmt: '#,##0' }, C2: { numFmt: '#,##0' }, D2: { numFmt: '#,##0' }, E2: { numFmt: '#,##0' },
B3: { numFmt: '#,##0' }, C3: { numFmt: '#,##0' }, D3: { numFmt: '#,##0' }, E3: { numFmt: '#,##0' },
B4: { numFmt: '#,##0' }, C4: { numFmt: '#,##0' }, D4: { numFmt: '#,##0' }, E4: { numFmt: '#,##0' },
B5: { numFmt: '#,##0' }, C5: { numFmt: '#,##0' }, D5: { numFmt: '#,##0' }, E5: { numFmt: '#,##0' },
A6: { bold: true }, B6: { bold: true, numFmt: '#,##0' }, C6: { bold: true, numFmt: '#,##0' }, D6: { bold: true, numFmt: '#,##0' }, E6: { bold: true, numFmt: '#,##0' },
}}
/>
One entry per address gets long by row six, which is what the document below is for. Formatting covers number formats, borders, merged headers and the rules that colour a cell by its value.
The document
Everything the shell shows beyond the cells lives in a sheet document:
per sheet its formats, column widths and row heights, hidden lines,
frozen panes, comments, protection, page setup, charts, validation and
conditional formatting rules. createSheetDocument builds one around a
workbook, doc.get(name) is a sheet's part of it, and the document
prop hands the whole thing to the component. formats.set formats a
rectangle at a time, which is how a header row or a money column is one
line:
<script lang="ts">
const wb = createWorkbook([{ name: 'Budget', cells: budget }])
const doc = createSheetDocument({ workbook: wb })
const sheet = doc.get('Budget')
sheet.formats.set([[0, 0, 0, 4]], { bold: true, fill: '#e2e8f0', color: '#0f172a' }, at)
sheet.formats.set([[1, 1, 5, 4]], { numFmt: '#,##0' }, at)
sheet.formats.set([[5, 0, 5, 4]], { bold: true, border: { top: { width: 1 } } }, at)
sheet.widths.A = 130
sheet.freeze = { rows: 1, cols: 1 }
</script>
<SvSheet document={doc} rows={12} columns={6} />
The header stays put when the sheet scrolls, because freeze is part of
the document too. So is what the user does next: a format from the
ribbon, a widened column, a new comment all land in doc, and
doc.getState() is the whole thing as plain JSON. onChange fires once
per tick with the reasons since the last call, so an autosave is one
handler:
<SvSheet document={doc} onChange={(reasons) => save(doc.getState())} />
Files takes it from there: what the state holds, restoring
it, .xlsx in and out, and the three things a network makes you think
about.
Rows into a sheet
A grid's rows, or an API's, become a sheet with sheetCellsFromRows: a
header row of labels, a row per record with each value as the text the
engine reads, and with totals a =SUM under every column that held a
number. It is how a dataset turns into something the user can add a
formula to.
<script lang="ts">
const orders = [
{ item: 'Desk', qty: 4, price: 320, shipped: true },
{ item: 'Chair', qty: 12, price: 85, shipped: false },
{ item: 'Lamp', qty: 6, price: 40, shipped: true },
]
const cells = sheetCellsFromRows(orders, [{ field: 'item', label: 'Item' }, 'qty', { field: 'price', label: 'Unit price' }, 'shipped'], { totals: true })
</script>
<SvSheet data={[{ name: 'Orders', cells }]} rows={10} columns={6} />
A boolean lands as TRUE or FALSE, a Date as yyyy-mm-dd, a null as
nothing.
The ribbon is Excel's
File, Home, Insert, Page Layout, Formulas, Data, Review and View, with
Excel's buttons under them and Excel's shortcuts beside them: Ctrl+B and the
Bold button call the same function, and the button lights up because the
cell is bold, not because it was clicked. Every action reaches an
onAction handler first, so an app can take one over (its own file
picker on Open, its own dialog on Goal Seek) by returning true.
Open the live example: Spreadsheet + Ribbon bar (Spreadsheet)
In a narrow frame, this page's examples included, a ribbon group that does not fit folds into one button named for the group, so Home > Cells
Format is a click deeper than on a full-width sheet. The
lookprop paints the shell with the app's--sg-*tokens ('theme', the default) or pins Excel's own palette and geometry ('excel').showRibbon,showFormulaBar,showTabsandshowStatusBarhide any part of the chrome, andheight="100%"fills a flex parent.
See also
- The spreadsheet shell - every prop, every dialog, the file formats and the limits, on one page.
- Formulas - references, names, LET and LAMBDA, your own functions, auditing.
- Formatting - number formats, borders, merged headers, conditional formatting.
- Files - save, restore, autosave,
.xlsx,.ods,.xlsand.csv. - Excel keyboard shortcuts - the full list, and how to turn them on for a plain grid.
<sv-sheet>- the same component as a custom element for React, Vue, Angular or plain HTML.
Live examples
- Blank sheet - just type - An empty Excel-style sheet on a plain <SvGrid>: column-letter headers (A..Z), a built-in 1..N row gutter, a name box + formula bar with a browsable function picker, gridlines, range selection and a fill handle. A real HyperFormula engine underneath: type a literal or a formula like =SUM(B2:D2) / =IF(...) and every dependent cell recalculates live. Drag a row or column border to resize; right-click for Cut / Copy / Paste / Clear.
- 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.
Related articles
- Render Your First Svelte Data Grid in Under 5 Minutes - How to add a fast, accessible, sortable data grid to a Svelte 5 app with SvGrid - covering data, typed columns, feature composition, and the imperative API.
- 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.
- How We Started Building SvGrid - The boundary we drew on day one - between the logic engine and the render layer - is what everything else rests on. Here is how that line was drawn and what it cost to get right.