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

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 &gt; 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>

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