Print preview
Google-Sheets-style full-screen print preview dialog with paginated previews, paper-size + margin + orientation settings, headers/footers, scope, and a paginated browser print
PrintPreviewDialog is a self-contained, full-screen modal that paginates the active sheet into printable page-sized blocks, renders the same blocks into a hidden iframe with the appropriate @page rules, and triggers the browser print dialog. Modelled on Google Sheets' Print pane — same sidebar controls, same paginated preview area, same paper sizes.
Is this a breaking change?
No. Print preview is purely additive:
New named export from
@rowsncolumns/spreadsheet:PrintPreviewDialogandPrintSettings/PaperSizeId/ related types.ButtonPrint's API is unchanged — it's still a thinToolbarIconButton. The only thing that updates is youronClickhandler (you used to callwindow.print(), now you open the dialog).No props on
useSpreadsheetState,CanvasGrid, or any existing component change. If you don't renderPrintPreviewDialog, nothing happens.
Upgrade path: drop the new dialog into your app and rewire ButtonPrint. That's it.
Why a dialog instead of window.print()?
window.print()?window.print() prints the entire host page — your toolbar, formula bar, sidebar, panels, ads, whatever else lives on the route. That's almost never what a user wants when they hit ⌘P on a spreadsheet.
The dialog instead:
Paginates the sheet data into page-sized blocks at the chosen paper size + orientation + scale.
Renders each block as a self-contained HTML table styled from
getEffectiveFormat.On Print, snapshots only those blocks into a hidden iframe alongside the matching
@page { size: ... }CSS, then callsiframe.contentWindow.print(). The browser print dialog shows the actual paginated spreadsheet, nothing else from your app.
Integration
The dialog reads everything it needs from values you already have on useSpreadsheetState. No adapter object; the props are flat.
That's the whole integration. The dialog manages its own settings state internally — the host doesn't need to persist or store anything. Settings carry over across open/close cycles within the same mount, so a user who picks A4 + Landscape and reopens still sees A4 + Landscape. If you want every open to start fresh, pass a changing key (e.g. key={isPrintOpen ? "open" : "closed"}) — React will remount the component and reset state to defaults.
PrintPreviewDialog props
PrintPreviewDialog propsisOpen
boolean
✅
Controlled visibility.
onClose
() => void
✅
Fires on Cancel, the close icon, or after Print. (Esc binding lives in user space — see Integration above.)
sheets
Sheet[]
✅
Full sheet list — same array on useSpreadsheetState.sheets. Drives both the active-sheet lookup and the "Workbook" scope.
activeSheetId
number
✅
Which sheet to preview. The dialog looks it up in sheets.
selections
SelectionArea[]?
The first selection's range powers the "Selection" scope. Pass useSpreadsheetState.selections straight through; omit / pass [] to hide the option.
title
string?
Workbook display name. Shown in the header strip when the "Workbook title" header field is enabled.
getFormattedValue
(sheetId, row, col) => string
✅
From useSpreadsheetState.
getEffectiveFormat
(sheetId, row, col) => CellFormat
✅
From useSpreadsheetState.
getDataRowCount
(sheetId) => number?
When present, restricts the print extent to the last non-empty row (matches Google Sheets). Falls back to sheet.rowCount.
getDataColumnCount
(sheetId) => number?
Same as above for columns.
getCellData
(sheetId, row, col) => CellData?
Surfaces the "Show notes" red corner marker. Without it the toggle still renders but produces no markers.
The dialog pulls everything else off the Sheet it resolves — title (display name), merges, frozenRowCount / frozenColumnCount, and per-row / per-column sizes from rowMetadata / columnMetadata.
Sidebar settings
The dialog manages its own settings state — defaults match Google Sheets where they line up.
Print scope
Current sheet · Workbook (when sheets.length > 1) · Selection (when selections[0] is set)
Paper size
Letter · Tabloid · Legal · Statement · Executive · Folio · A3 · A4 · A5 · B4 · B5
Orientation
Portrait · Landscape
Scale
Normal (100 %) · Fit to width · Fit to page · Custom (10 – 400 %)
Margins
Normal · Narrow · Wide · Custom (per-edge px)
Show gridlines
toggle
Show notes
toggle — paints a red corner triangle on noted cells (needs getCellData)
Page order
Over, then down (default) · Down, then over
Horizontal alignment
Left · Center · Right
Vertical alignment
Top · Middle · Bottom
Headers & footers
Page numbers · Workbook title · Sheet name · Current date · Current time — auto-placed across the 3-zone strip
Repeat frozen rows
toggle, disabled when Sheet.frozenRowCount is 0 / unset
Repeat frozen columns
toggle, disabled when Sheet.frozenColumnCount is 0 / unset
Pagination
The pagination engine (computePageLayout) walks the cell range with the active scale, splitting rows and columns into bands whose summed scaled dimensions fit the page content area (paper minus margins minus optional header/footer strips, minus a 1 px reserve for the closing cell border).
Over, then down: outer = row bands, inner = column bands — page order goes left-to-right across the first row band, then advances. (Google Sheets' default.)Down, then over: outer = column bands, inner = row bands — page order goes top-to-bottom within the first column band, then advances. (Excel'spageOrder="downThenOver"default.)
Frozen rows / columns, when their toggle is on, are subtracted from the content area on every page and prepended as a fixed prefix to each band — every page renders with the same frozen header rows / leftmost columns.
What's NOT in this release
Deferred so we could ship the core flow. None block the integration above.
Set custom page breaks — drag-edit page breaks UI. The underlying
<rowBreaks>/<colBreaks>XLSX metadata still round-trips on import/export.Edit custom fields — the Google-Sheets 3-zone (
&L/&C/&R) header / footer composer. v1 auto-places enabled fields.Show notes — note text — the toggle ships and marks noted cells with a red corner triangle, but the note text isn't emitted in a trailing endnote section (Google Sheets does this; deferred).
Files
apps/spreadsheet/components/print-preview/settings.ts—PrintSettingsmodel + paper-size / margin constants.apps/spreadsheet/components/print-preview/use-page-layout.ts— pure pagination math; band slicing, scale resolution.apps/spreadsheet/components/print-preview/cell-styles.ts— mapsCellFormatto inline CSS for the printed cells.apps/spreadsheet/components/print-preview/page-block.tsx— renders one page as a styled HTML table.apps/spreadsheet/components/print-preview/print-preview-dialog.tsx— the modal, sidebar, and pagination orchestration.apps/spreadsheet/components/print-preview/run-print.ts— hidden-iframewindow.print()driver with@pagerules.
Last updated