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. (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
An expense review sheet on the Svelte 5 spreadsheet shell, opened protected: every cell is locked except the reviewer's Receipt and Status columns, so typing over an amount is refused with a message while a status picked from the in-cell list goes through and moves the SUMIF totals. Three reviewer comments sit on the cells they concern as Excel's red corner, two of them threads with a signed reply and one resolved: hover for the text, Shift+F2 to open the box, Reply to answer, Review > Next to walk them, Show All Comments to list them. Protection, the unlocked cells, the notes and the rules are part of the document and come back from getState(). Enterprise, in @svgrid/enterprise.
The expense sheet a finance reviewer works through. Ten claims came in from the team; the reviewer decides each one, leaves a note where something is off, and nobody else can touch the figures while it is under review:
Protection the sheet opens protected. Every cell is locked except the two the reviewer owns, Receipt and Status, so a keystroke on an Amount does nothing and the status bar says why. Review > Unprotect Sheet opens everything; Format Cells > Protection shows which cells were unlocked. The Protect Sheet list leaves Format rows and Sort open, and Allow Edit Ranges names the Notes column, which takes an edit although its cells are locked. Comments three reviewer comments sit on the cells they are about, the red corner Excel draws. Two are threads with a reply, one of them resolved; the third is a plain note. Hover for the text, Shift+F2 to open the box, Reply to answer, Review > Next to walk them, Show All Comments to list them. commentAuthor signs what the reviewer writes. Validation Status is a list, so the reviewer picks Approved / Rejected / On hold from the arrow rather than typing it. Formulas the totals by status are SUMIFs, so a decision moves the money at the bottom as it is made.
Everything here is part of the document: protection, the unlocked cells, the notes and the rules ride through getState() with the cells.
Try: type over an Amount (refused), then pick "Approved" on row 3 and watch the totals. Review > Next lands on the first note; Ctrl+Enter in the box saves an edit. Unprotect Sheet, then the Amount takes the edit.
Imports, features and API used
Imports: @svgrid/enterprise
Frequently asked questions
How are some cells editable on a protected sheet?
Every cell is locked to begin with, as in Excel. The Receipt and Status cells carry locked: false in their format entry (Format Cells > Protection, or the formats prop), and Review > Protect Sheet then locks everything else. Unprotect Sheet opens the whole sheet again.
Where do the comments live?
In the document, per sheet, keyed by cell: getState().sheets.Claims.comments. A note is its text; a thread is an object with the text, author, time, replies and resolved flag. They move with an insert or delete, a saved document comes back with them, and Save As writes a thread as Excel's threaded comment.
Can a comment be edited from the keyboard?
Shift+F2 opens the note box on the active cell, Ctrl+Enter saves it, Escape closes it and keeps the text as Excel does. Review > Previous and Next jump between commented cells.
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.
- 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.
- 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.
Source code (460-review-comments-protection.svelte)
<script lang="ts">
/**
* 460. Cell comments and Protect Sheet: a review workflow
* ------------------------------------------------------
* The expense sheet a finance reviewer works through. Ten claims came in
* from the team; the reviewer decides each one, leaves a note where
* something is off, and nobody else can touch the figures while it is
* under review:
*
* Protection the sheet opens protected. Every cell is locked except
* the two the reviewer owns, Receipt and Status, so a
* keystroke on an Amount does nothing and the status bar
* says why. Review > Unprotect Sheet opens everything;
* Format Cells > Protection shows which cells were unlocked.
* The Protect Sheet list leaves Format rows and Sort open,
* and Allow Edit Ranges names the Notes column, which takes
* an edit although its cells are locked.
* Comments three reviewer comments sit on the cells they are about,
* the red corner Excel draws. Two are threads with a reply,
* one of them resolved; the third is a plain note. Hover for
* the text, Shift+F2 to open the box, Reply to answer,
* Review > Next to walk them, Show All Comments to list them.
* `commentAuthor` signs what the reviewer writes.
* Validation Status is a list, so the reviewer picks Approved /
* Rejected / On hold from the arrow rather than typing it.
* Formulas the totals by status are SUMIFs, so a decision moves the
* money at the bottom as it is made.
*
* Everything here is part of the document: protection, the unlocked
* cells, the notes and the rules ride through getState() with the cells.
*
* Try: type over an Amount (refused), then pick "Approved" on row 3 and
* watch the totals. Review > Next lands on the first note; Ctrl+Enter in
* the box saves an edit. Unprotect Sheet, then the Amount takes the edit.
*/
import { SvSheet, createWorkbook, createSheetDocument, type CellFormatEntry } from '@svgrid/enterprise'
const CLAIMS: ReadonlyArray<readonly [string, string, string, number, string, string]> = [
['EXP-2041', 'Ana Ruiz', 'Travel', 412, 'Yes', 'Approved'],
['EXP-2042', 'Ben Okafor', 'Meals', 210, 'Yes', 'Submitted'],
['EXP-2043', 'Chloe Adams', 'Software', 89, 'Yes', 'Approved'],
['EXP-2044', 'Dev Patel', 'Travel', 1180, 'No', 'On hold'],
['EXP-2045', 'Ana Ruiz', 'Lodging', 640, 'Yes', 'Submitted'],
['EXP-2046', 'Ben Okafor', 'Travel', 412, 'Yes', 'Submitted'],
['EXP-2047', 'Eli Novak', 'Training', 950, 'Yes', 'Approved'],
['EXP-2048', 'Chloe Adams', 'Meals', 58, 'No', 'Rejected'],
['EXP-2049', 'Dev Patel', 'Software', 240, 'Yes', 'Submitted'],
['EXP-2050', 'Eli Novak', 'Lodging', 520, 'Yes', 'Submitted'],
]
const LAST = CLAIMS.length + 1 // the last claim is on row 11
const STATUSES = ['Submitted', 'Approved', 'Rejected', 'On hold']
const claims = [
['Claim', 'Employee', 'Category', 'Amount', 'Receipt', 'Status', 'Paid', 'Notes'],
...CLAIMS.map(([id, who, cat, amount, receipt, status], i) => [
id, who, cat, String(amount), receipt, status, `=IF(F${i + 2}="Approved",D${i + 2},0)`,
]),
[],
['By status', 'Claims', 'Amount'],
...STATUSES.map((s, i) => [s, `=COUNTIF(F2:F${LAST},A${i + 14})`, `=SUMIF(F2:F${LAST},A${i + 14},D2:D${LAST})`]),
['Total', `=SUM(B14:B17)`, `=SUM(C14:C17)`],
[],
['Approved so far', `=SUM(G2:G${LAST})`],
]
const wb = createWorkbook([{ name: 'Claims', cells: claims }])
// The document carries what is not a cell: the notes, the rules and the
// protection flag. Built here so the sheet opens the way the reviewer
// left it, exactly as a saved document would come back.
const doc = createSheetDocument({ workbook: wb })
const sheet = doc.get('Claims')
// Two are threads, Excel's threaded comments: an author and a time on
// each entry, replies under the first, one resolved. The third is a
// plain note, the shape every earlier document carries.
sheet.notes = {
r2: { D: {
text: 'Three days of meals at 70 a day: over the 60 limit. Needs a director\'s sign-off before it goes through.',
author: 'Finance review', at: '2026-03-02T09:10:00.000Z',
replies: [{ text: 'Signed off by M. Chen this morning, see the ticket.', author: 'Ben Okafor', at: '2026-03-03T08:05:00.000Z' }],
} },
r4: { E: {
text: 'No receipt attached. On hold until it arrives; the airline can reissue one.',
author: 'Finance review', at: '2026-03-02T09:14:00.000Z', resolved: true,
replies: [{ text: 'Reissued receipt attached now.', author: 'Dev Patel', at: '2026-03-04T16:40:00.000Z' }],
} },
r6: { A: 'Same fare and dates as EXP-2041 on row 2. Check it is not the same ticket claimed twice.' },
}
sheet.validation = [
{ id: 'status', rects: [[1, 5, LAST - 1, 5]], allow: 'list', value1: STATUSES.join(','), ignoreBlank: true, inCellDropdown: true, alert: { style: 'stop', title: 'Status', message: 'Pick one of Submitted, Approved, Rejected or On hold.' } },
{ id: 'receipt', rects: [[1, 4, LAST - 1, 4]], allow: 'list', value1: 'Yes,No', ignoreBlank: true, inCellDropdown: true, alert: { style: 'stop' } },
]
sheet.conditionalFormats = [
{ id: 'ok', rects: [[1, 5, LAST - 1, 5]], kind: 'text', match: 'contains', value: 'Approved', style: { fill: '#C6EFCE', color: '#006100' } },
{ id: 'no', rects: [[1, 5, LAST - 1, 5]], kind: 'text', match: 'contains', value: 'Rejected', style: { fill: '#FFC7CE', color: '#9C0006' } },
{ id: 'hold', rects: [[1, 5, LAST - 1, 5]], kind: 'text', match: 'contains', value: 'On hold', style: { fill: '#FFEB9C', color: '#9C5700' } },
]
sheet.freeze = { rows: 1, cols: 0 }
sheet.protected = true
// Excel's Protect Sheet list and Allow Edit Ranges: the reviewer may
// still size and hide rows and sort the claims, and the Notes column
// takes an edit although its cells are locked.
sheet.protection = {
allow: { formatRows: true, sort: true },
ranges: [{ id: 'notes', title: 'Reviewer notes', rects: [[1, 7, LAST - 1, 7]] }],
}
// Fills are literal colours, as in Excel, each with its own text colour so
// the sheet reads the same on a dark theme.
const BAND = { bold: true, fill: '#e2e8f0', color: '#0f172a' } as const
const MONEY = { numFmt: '$#,##0' } as const
// The two columns the reviewer owns: unlocked, and tinted the way Excel
// templates mark their input cells.
const INPUT = { locked: false, fill: '#fffbe6', color: '#0f172a' } as const
type Entry = Record<string, CellFormatEntry>
const across = (cols: string, row: number, entry: CellFormatEntry): Entry =>
Object.fromEntries([...cols].map((c) => [`${c}${row}`, entry]))
const down = (col: string, from: number, to: number, entry: CellFormatEntry): Entry =>
Object.fromEntries(Array.from({ length: to - from + 1 }, (_, i) => [`${col}${from + i}`, entry]))
const formats: Entry = {
...across('ABCDEFG', 1, BAND),
...down('D', 2, LAST, MONEY), ...down('G', 2, LAST, MONEY),
...down('E', 2, LAST, INPUT), ...down('F', 2, LAST, INPUT),
...across('ABC', 13, BAND),
...down('C', 14, 18, MONEY),
...across('ABC', 18, { bold: true }),
A20: { bold: true }, B20: { ...MONEY, bold: true, fontSize: 15 },
}
</script>
<section class="wrap flex flex-col flex-1 min-h-0">
<SvSheet document={doc} height="100%" rows={24} columns={9} columnWidths={{ A: 130, B: 130 }} {formats} commentAuthor="Finance review" />
<p class="note shrink-0">
The sheet opens <strong>protected</strong>: only the tinted Receipt and
Status cells take an edit. Pick a status from the arrow on row 3 and the
totals follow. The red corners are the reviewer's notes: hover one, or
press <strong>Review > Next</strong> to walk them. Unprotect Sheet on
the Review tab opens the rest.
</p>
</section>
<style>
.wrap { gap: 8px; }
.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.
- 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.
- Excel keyboard shortcuts - The muscle memory a spreadsheet user arrives with: Ctrl+Arrow jumps to the edge of the data region (a run-boundary search, so it hops gaps rather than running to the end), Ctrl+Shift+Arrow extends there, Ctrl+A takes the current region then the sheet, Ctrl+Space / Shift+Space take the column or row, Ctrl+D / Ctrl+R fill, Ctrl+; stamps the date. One enableSheet() call. Ctrl+Z undoes a whole fill in one press, not one per cell.
- Formula bar + cell formats - The Excel cell experience: a formula bar showing the RAW text behind the active cell (the grid shows 1,234.50, the bar shows =B2*C2) with function autocomplete and signature hints, a Name Box that jumps to an address, and number formats that live on the CELL rather than the column. Ctrl+Shift+4 for currency, Ctrl+B to bold. The format store keys on row id, so sorting does not leave formatting behind on the old index.