ReoGrid Web is a JavaScript/TypeScript spreadsheet library that puts an Excel-grade editing experience — formula engine, xlsx I/O, canvas renderer — into a single dependency-free package, with first-class React and Vue wrappers and a free Lite tier on npm.
v1.4 turned the grid into a document tool: page layout, a page-break preview, PDF export, pivots. v1.5 goes back to the gestures. The two things a user reaches for within a minute of opening a real spreadsheet — Ctrl+F and dragging a block somewhere else — are now built in, in both editions. On the paper side, pages can carry a header and footer written in Excel’s own format codes, and PDF export finally knows how to fetch its own CJK fonts: name a locale, get the right font. Chinese exports no longer come out as tofu.
Underneath sits a batch of fixes from the public issue tracker — overflowing text vanishing on scroll, dropdowns opening off-screen, the last row hidden behind a scrollbar, readReoGridJson merging documents instead of replacing them.
It is additive: there are no breaking public-API changes.
Find & replace — and it’s in Lite too
Press Ctrl/Cmd+F (find) or Ctrl/Cmd+H (replace) in a focused grid. That’s the whole setup. The bar searches the sheet, the current selection, or the whole workbook — activating whichever sheet a match lands on, so the counter reads 2/4 · Sheet1 — with match-case, entire-cell (セル内容が完全に同一) and regular-expression options. Every hit is highlighted on the canvas, Enter and Shift+Enter walk through them, and Escape clears.
Replace and replace-all are one undo step, and they skip protected cells. A search over displayed values (lookIn: 'value') never rewrites a formula cell whose result matched — a replace always writes back to the raw input, mirroring Excel, whose 置換 operates on formulas.
The same operations are available programmatically, so a host can drive its own UI:
import { createReogrid } from '@reogrid/pro';
// Turn the built-in bar off and wire your own toolbar
const grid = createReogrid({ workspace: '#app', showFindBar: false });
const first = grid.find('Tokyo'); // FindMatch | null → { row, column, text }
grid.findNext();
grid.findPrevious();
const all = grid.findAll('Tokyo', { matchCase: true }); // pure read, no session
const n = grid.replaceAll('Tokyo', 'Osaka', { wholeCell: true }); // → cells changed
grid.clearFind();
Hidden lines, merged followers and unloaded delay-load rows are skipped by default (includeHidden opts back in). This one is in both editions — searching is basic editing, like copy/paste. See the find & replace docs and the live demo.
Drag to move — ranges, rows, and columns
One gesture, two operations, exactly as in Excel:
| Gesture | Operation | Semantics |
|---|---|---|
| Drag the selection border | Move a cell block | Overwrites the destination |
| Drag an already-selected row/column header | Reorder whole lines | Cut-and-insert — nothing is lost |
Grab the selection’s border (a ±3px band just inside it) and a preview rectangle follows the pointer. The fill handle on the bottom-right corner still wins, so auto-fill is untouched. Pressing an already-selected row or column header arms a reorder that only starts after 4px of travel, so a plain click still just re-selects the line.
Everything stored per cell travels together — value, formula text, rich text, number format, style, cell type, border, comment, and the locked flag.
const ws = grid.worksheet;
// Relocate a block — its top-left lands on (row, column). Overwrites.
ws.range('A1:C3').moveTo(10, 0);
// Reorder whole lines (cut-and-insert)
ws.moveRows(2, 3, 8); // 3 rows starting at index 2 → before index 8
ws.moveColumns(1, 1, 4); // 1 column at index 1 → before index 4
// Ask before you move
const check = ws.canMoveRange({ topRow: 0, leftColumn: 0, bottomRow: 2, rightColumn: 2 }, 10, 0);
if (!check.ok) console.warn(check.reason);
A move that would tear a merged cell, land out of bounds, hit a protected cell, or run while a delay-load source is attached is refused — and the refusal is legible: onBeforeRangeMove still fires with cancel already set and a blockedReason ('merge-source', 'out-of-bounds', 'protected', …) you can turn into a message. The same event is cancellable, so a host can substitute its own behavior:
grid.onBeforeRangeMove((event) => {
if (event.kind === 'rows' && event.from.topRow === 0) {
event.cancel = true; // pin the header row
}
});
Every move is a single undo entry, and worksheet.setRangeMoveEnabled(false) turns the interaction off without touching the API. Also in both editions.
Two limits worth knowing up front: v1 does not rewrite formulas that reference the moved cells (the same behavior as the existing cut/paste), and a row/column reorder does not carry conditional-format, validation, table, page-break, outline or filter ranges. See the move docs and the demo.
Page header & footer — the paper edges, in Excel’s codes
PrintSettings.headerFooter puts text in the paper margins, with left / center / right sections per band:
ws.setPrintSettings({
headerFooter: {
header: { center: '売上明細' },
footer: { right: 'Page &P / &N', left: '&D' },
},
});
Section text holds Excel’s format codes verbatim — &P (page, &P+2 offsets included), &N (page count), &D / &T, &F (file name), &A (sheet name), &Z (path), && for a literal & — so a round-trip through Excel keeps them intact. Styling codes (&B, &12, &"Meiryo,Bold", &KFF0000, &G) are recognised and dropped, since one font weight is embedded; an unknown code is kept verbatim, so a typo stays visible instead of silently vanishing.
differentOddEven and differentFirst enable the evenHeader / evenFooter and firstHeader / firstFooter bands — mirrored margins in a bound document, or a cover page with no page number.
The band sits at margins.header / margins.footer (Excel’s 0.3 in default, now honored rather than hardcoded), and a tall band pushes the body margin out — so it costs rows per page instead of printing over your cells. The break computation, PDF export and the paged print HTML all agree on the resulting geometry. Both saveAsPdf and the print HTML draw the bands; pass headerFooter: null to export without them. Round-trips through xlsx <headerFooter> and ReoGrid JSON.
One note: the on-screen page-break preview does not draw the bands — only the body area it outlines shrinks. Print or export to see them. Pro. See the page layout docs.
PDF fonts by locale — the end of tofu
The PDF engine embeds glyph outlines, so an export has always needed real font bytes. Until now the only bundled loader was loadDefaultJapaneseFont() — which covers about 35% of common simplified-Chinese business vocabulary. Exporting a Chinese sheet produced pages of tofu (□□□) with no supported way to fix it.
v1.5 adds a font registry. Name a locale and ReoGrid fetches and embeds the right font:
import { createReogrid, preloadPdfFont } from '@reogrid/pro';
const grid = createReogrid({ workspace: '#app' });
await preloadPdfFont('zh-CN'); // once, at an async point you control
grid.saveAsPdf({ locale: 'zh-CN', filename: 'report.pdf' });
ja, zh-CN, zh-TW and ko are registered out of the box, and registerPdfFont adds your own — a loader, a URL, or bytes you already hold.
Why the separate preload step? Because export stays synchronous on purpose: saveAsPdf ends in a click on a generated link, which loses its user-gesture context — and meets the popup blocker — if an await runs first. So you pay the download once, at a point you choose, and every later export is instant. font still takes explicit bytes for a font ReoGrid does not know about, and wins when both are given; one of the two is required.
For production, don’t rely on the built-in URLs: they point at the full upstream fonts on a public CDN (Noto Sans SC alone is ~17 MB, and that CDN is slow and intermittently unreachable from mainland China). Install the matching subset package instead — the bytes come from npm, so there is no runtime network at all, which also works on the offline intranets common in enterprise deployments:
import { registerPdfFont, preloadPdfFont } from '@reogrid/pro';
import { loadNotoSansSC } from '@reogrid/font-sc'; // also font-tc, font-jp, font-kr
registerPdfFont('zh-CN', loadNotoSansSC);
await preloadPdfFont('zh-CN');
@reogrid/font-sc is ~2.6 MB over the wire for the full GB 2312 set, against ~17 MB for the upstream font. Pro. See the PDF export docs.
And a dozen fixes from the issue tracker
Most of these came in as real reports, and several were quiet wrong-output bugs rather than crashes:
- Overflowing text kept rendering after its own cell scrolled out of view. A long value spilling across the empty cells to its right vanished entirely once its own column left the viewport.
- A dropdown near the bottom of the window now opens upward instead of landing past the screen edge where no scrolling could reach it — both cell dropdowns and the auto-filter popup.
- A floating image no longer covers the native scrollbars, and the last row/column is no longer hidden behind one on hosts with classic space-reserving scrollbars (Windows, or macOS set to always show them).
- Freezing panes no longer leaves dead travel at the end of the scrollbar — the scroll spacer is re-computed on structure changes, so the native and worksheet maxima agree immediately.
readReoGridJson()replaces the worksheet instead of merging into it, so loading a second document no longer leaves the previous one’s rows, styles and merges underneath — andwriteReoGridJson(worksheet)keeps the sheet’s name instead of renaming it toSheet1on every save.- A number-formatted formula cell no longer keeps a stale value after a bulk recalculation (sort, JSON load, report bind, or a move).
- PDF: B4 / B5 / Tabloid / Executive are honored in
pageSizeinstead of silently falling back to A4 — JIS B4/B5 being everyday paper in Japan made this a quiet wrong-output bug for exactly the customers most likely to hit it. Grid lines now follow the sheet’s own setting, a border on a page boundary prints on both pages, and passing a font family name (font: 'Arial') is rejected with a message that points atloadFont(url)orlocale, instead of aRangeErrorfrom inside the font parser. worksheet.selectionis in the published type declarations — the documented façade worked at runtime but was missing from the bundled.d.ts.- Removing an outline group gives its rows/columns back, and the group bracket’s closing tick is drawn at the far end, away from the toggle button, matching Excel.
clearRowOutlines()/clearColumnOutlines()drop every group in one call.
Two behavior changes worth flagging: double-clicking a column/row border now auto-fits the whole selection (select A:D, double-click any of their borders, all four are fitted — as in Excel), and the grid re-scales on a HiDPI monitor change, so dragging the window between a Retina and a standard display no longer leaves the canvas blurry until the next manual resize.
Upgrade
v1.5 is a drop-in upgrade — no code changes required.
yarn up @reogrid/pro@latest
Lite is on npm with no license key:
npm install @reogrid/lite
See the release notes for the complete v1.5 changelog, the find & replace, range move, page layout and PDF export docs for the full APIs, or try the find & replace, range move and PDF print demos. The pricing matrix has the Lite vs Pro breakdown.