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. v1.5 filled in the everyday gestures. v1.6 is about the things the grid was quietly throwing away.
Three of them, specifically. A picture loaded from an xlsx was displayed and then dropped on save. A cell comment could be read but never written back, so saving a reviewed workbook erased every note. A print area round-tripped through our own JSON but not through xlsx, so the next open printed the whole sheet again. All three now survive a save — and images went from read-only to fully editable in the same release.
Alongside that: multi-range selection with Ctrl/Cmd-click, 縦書き and text rotation, a form mode that turns a protected sheet into a keyboard-only fill-in form, Excel’s fit to N pages, and the fix for a bug that made every exported PDF come out hairline.
It is additive: there are no breaking public-API changes.
Images: insert, move, resize — and they stay
Until now images were a viewer feature. You could load an xlsx and see the logo; you could read the bytes back out. What you could not do was add one, move one, or save the file without losing it.
import { createReogrid } from '@reogrid/pro';
const grid = createReogrid({ workspace: '#app' });
const ws = grid.worksheet;
// Bytes, an ArrayBuffer, or a data: URL. Returns the new image's id.
const id = ws.addImage(bytes, { row: 1, column: 1, width: 240, height: 120 });
// Or straight from a file picker — defaults to the image's natural size
await ws.addImageFromFile(file, { row: 1, column: 1 });
ws.moveImage(id, { row: 5, column: 2 });
ws.resizeImage(id, { width: 320, height: 160 });
ws.removeImage(id);
With the mouse: click to select and eight grips appear, drag the body to move, drag a grip to resize (Shift keeps the aspect ratio), Delete to remove. Each drag is a single Ctrl+Z step — not one per pixel of travel.
Images anchor to cells the way Excel’s do, in all three OOXML flavours — oneCell (moves with the cell, fixed size), twoCell (moves and sizes with the cells) and absolute (pinned to the sheet) — and they follow row and column inserts and deletes. Deleting the rows under a logo no longer deletes the logo; a twoCell image shrinks with the rows it spans, exactly as Excel behaves.
The persistence is the real change. Export now writes real xl/media and xl/drawings parts, with identical images stored once, so a stamped invoice opens in Excel with its stamp. They round-trip through ReoGrid JSON too (sheet.images; pass toJson({ includeImages: false }) when the JSON is a wire format and a photo would dwarf it), and they are drawn in PDF export.
Two switches control the interaction without touching the model. ws.setImageEditEnabled(false) leaves the images visible and the API usable but takes the mouse out of it — sheet protection does the same implicitly. ws.setImagesEnabled(false) turns the feature off altogether, which is what Lite does internally: Lite does not display or edit images, but import and export still carry them, so a document opened in a Lite viewer never loses its pictures.
Pro. See the images docs and the live demo.
Multi-range selection — in both editions
Hold Ctrl (Cmd on macOS, where Ctrl+click is a right click) and click or drag to add a range instead of replacing it — on cells and on row and column headers alike. Pick four scattered branches out of a regional sales sheet and bold them in one go.
Everything that acts on “the selection” now covers all of it: Delete clears every range as one undoable step, styles apply to all of them, and double-clicking the border of Ctrl-clicked columns auto-fits each one — and nothing in between.
const ws = grid.worksheet;
ws.selection.moveTo('A1:B2');
ws.selection.add('D4:E6'); // the programmatic form of Ctrl+click
ws.selection.count; // 2
ws.selection.ranges.forEach(r => r.setBold());
ws.selection.ranges is the safe way to act on what the user selected, because a single-range selection simply yields one handle. bounds / range / activeCell / moveTo keep their single-range meaning — they refer to the active range, the last one selected — and onSelectionChange gains a second argument listing every range, so existing single-argument listeners are unaffected.
Copy follows Excel’s rule: the ranges have to line up — stacked with the same columns, or side by side with the same rows — to travel as one block. Anything else is refused, with Excel’s own explanation shown on the cell. While several ranges are selected the fill handle and drag-to-move are switched off.
Both editions. See the selection docs and the live demo.
縦書き and text rotation — in both editions
Two things that a Japanese business form needs and a web grid usually cannot do.
ws.range('B1:F1').setTextRotation(45); // Excel's -90..90; out-of-range values clamp
ws.range('A1:A6').setVerticalText(); // 縦書き
Rotation is the layout that makes a wide table of narrow numeric columns readable: the headers are long words, the data is four digits, and turning the headers 45° lets the columns stay the width the numbers actually need. Vertical writing stacks characters top-to-bottom with columns running right to left, turns long vowel marks and brackets to their vertical forms, and puts 、 and 。 at the top-right of the character box, as vertical typesetting requires. It is what 年度 or 第一四半期 needs when the column is one character wide.
The part that makes it usable rather than a novelty: auto-fit measures the rotated outline. A label turned 90° gets a column only as wide as the row is tall — not a column wide enough for the horizontal text, which would defeat the point of turning it.
Screen, xlsx, browser print and PDF export all produce the same placement, and the style round-trips as Excel’s own <alignment textRotation> (255 for vertical). Rotation and vertical writing are one control, exactly as in Excel’s alignment dialog: setting an angle clears vertical writing, and vice versa.
Both editions. See the text rotation docs and the live demo.
Form mode — a protected sheet you fill in from the keyboard
A business app often uses a spreadsheet as a form, not as a spreadsheet. The layout is fixed, most cells are protected, and the user only fills values into a handful of boxes. That shape used to need a pile of host-side event handling. Now it is four switches:
const ws = grid.worksheet;
ws.protected = true;
ws.range('C3:C8').setLock('unlocked'); // the input cells
ws.layoutLocked = true; // no header drag-resize, auto-fit, or line reorder
ws.clickToEdit = true; // one click opens the editor, caret at the end
ws.formNavigation = true; // Enter / Tab walk the input cells
ws.setCellPlaceholder(2, 2, 'e.g. Taro Yamada');
clickToEdit also shows an I-beam over editable cells, so the box looks like the input it behaves as. formNavigation uses a reading-order scan that skips protected cells, hidden rows and columns and merge interiors, and wraps at the end of a row — which is what carries the user from the last field of one row to the first field of the next. When your app knows its own layout, ws.setFormNavigator(fn) replaces that with a deliberate order; returning null stops at the end of the form.
Placeholders are drawn only while the cell is empty and are never part of the value — they do not appear in PDF, print, xlsx or JSON, and they are not right-aligned as numbers.
The last mile is the dropdown. List validation can now be driven entirely from the keyboard: ↓ while editing, or Alt+↓ while the cell is selected, opens the same list the mouse opens; ↑↓ move, Enter commits, Escape closes. The list opens at the current value, so ↓ then Enter is a two-keystroke pick — and because the choice commits through the editor, validation and form navigation both still apply. A form now fills in end-to-end without ever reaching for the mouse.
None of these switches is persisted, so re-apply them after loadJson() or reset().
Both editions. See the form mode docs and the live demo.
Printing: fit to N pages, and two things that finally reach xlsx
fitToPages is Excel’s 「N ページに収める」. Instead of picking a shrink percentage by hand, say how many pages wide or tall the sheet should come out:
ws.setPrintSettings({ fitToPages: { width: 1 } }); // all columns on one page
ws.getEffectivePrintScale(); // → the derived scale, e.g. 0.72
Like Excel it only ever shrinks — content that already fits prints at 100% — and the page-break preview, PDF export and browser print all agree on the resulting number. Your configured scale is remembered while fit mode is on and comes back when you turn it off.
Two long-standing gaps closed alongside it:
- The print area is written to xlsx.
setPrintableRange(...)used to round-trip through ReoGrid JSON only, so saving to xlsx printed the whole sheet again on the next open. It now travels as Excel’s own_xlnm.Print_Area. - Cell comments survive a save. Export writes the comments part and the legacy VML box Excel expects alongside it, so a note round-trips with its text, its author, and whether it is pinned open. Nothing to call — it happens on
saveAsXlsx.
Page-break and print-area drags are undoable now, too: one drag, one Ctrl+Z, and undoing a break move also restores the print scale that drag lowered.
Pro. See the page layout docs.
Why every PDF came out hairline
This one is worth telling, because the symptom pointed nowhere near the cause.
Exported PDFs were thin. Not “slightly light” — the whole page rendered at Thin weight, and bold headings were not visibly bold. It reads like a CSS problem, or a scaling problem, and it is neither.
The built-in font tags pointed at Google’s variable fonts. A variable font stores its outlines at the fvar default position in glyf, and the deltas that move them to other weights in gvar. A PDF embeds glyf outlines; the exporter does not apply gvar. Noto’s default position is wght=100, so every document was baked at Thin — visible in the output as /BaseFont /…+NotoSansJP-Thin. And because bold was a synthetic 4% stroke of whatever outline it was handed, it had a hairline outline to thicken.
Since a PDF weight is a separate font file, the registry now holds a face per weight:
import { registerPdfFont, preloadPdfFont } from '@reogrid/pro';
import { notoSansJP } from '@reogrid/font-jp'; // { thin, normal, bold }
registerPdfFont('ja', notoSansJP);
await preloadPdfFont('ja'); // fetches normal + bold by default
The built-in URLs now point at static faces, and the JP payload dropped from one 9.15 MB variable font to about 2.5 MB per face. preloadPdfFont still resolves with the regular bytes, so calls written before v1.6 are unchanged, and registerPdfFont(tag, oneSource) still registers that source as normal. Synthetic bold is demoted to what it always should have been: the fallback used only when there is no bold face.
If you loaded your own font, options.fonts adds the other weights:
grid.saveAsPdf({
font: await loadFont('/Brand-Regular.ttf'),
fonts: { bold: await loadFont('/Brand-Bold.ttf') },
});
The @reogrid/font-{jp,sc,tc,kr} packages ship Thin/Regular/Bold static faces as of v2 and export an object of exactly the shape registerPdfFont wants, so hand over notoSansJP rather than a single loader — a single loader still works but registers only the regular face, which leaves bold to the synthetic fallback.
Pro. See the PDF export docs.
Browser print now matches what the canvas draws
Printing through HTML was losing text that was visible on screen — 「有効期限」 coming out as 「有…」. Three separate mismatches, all fixed:
- Text spills into empty neighbours again. The canvas, like Excel, lets a value wider than its column overflow into the empty cells beside it; a
<td>always clips. The overflow width is now computed with the same function the screen uses. - No more ellipsis. A 2px overhang used to turn the last character into
…. Neither the canvas nor Excel does that on paper — they clip. - Rows are exactly their specified height.
<tr height>is a minimum in HTML, so a wrapping detail row stretched and pushed everything below it down until the rows at the foot of the page — 振込先, 備考 — fell off the paper entirely.
Cell-anchored images are drawn on this route now as well, so a logo or 印影 that appears on screen and in PDF also appears on the printed page. Content taller than its row is clipped to the row instead of spilling over its neighbours, and PDF export honours a number format’s bracket colour ([Red] / [赤]), so the red negatives on an accounting report stop printing black.
And the rest
- Enter no longer doubles a value. Typing
120⏎180⏎ into one cell produced120180: the editor committed without moving, and the same keydown reached the keyboard controller, which reopened the editor on the same cell with the committed text still in it. Enter now commits and moves down, Shift+Enter up, Tab right, Shift+Tab left. - Thousands scaling works with decimals.
#,##0,worked, but0.0,and#,##0.0,,— a comma after the decimal part, which is the arrangement Excel itself writes — did not scale, and the comma was not stripped either, so0.0,,"M"displayed 2,400,000 as2400000.0,,M. - 和暦の元年.
[$-ja-JP-x-gannen]ggge"年"m"月"d"日"now prints 令和元年7月1日 rather than 令和1年 — required on 官公庁 and 金融 forms. - Sorting carries comments, cell types and lock state.
sortRowsreordered values, styles and borders but left notes behind on the original rows, so after a sort a comment appeared to belong to a different row’s content. - Text in a hidden column no longer paints over the column beside it — which is what happened to imported templates that hide their working columns.
- Page breaks follow a one-at-a-time size change.
worksheet.column(c).width = wdid not rebuild the boundaries, sogetPrintPageRanges()returned breaks computed from the old sizes. Usually hidden by the next paint in a browser; always wrong headless. autoFitMaxScanRowsis reachable fromcreateReogrid(). Added toWorksheetOptionsin v1.5, it was never declared on the public options type, so package users could not set it.- Row/column header font.
createReogrid({ headerFont: { fontFamily: 'Meiryo', fontSize: 14 } })— the row numbers and column letters were fixed at12px sans-serif, so changing the body font left the headers behind. The header band widens to fit. - Headless xlsx import.
XlsxImporterandconvertImageInfoare exported from the public entry, for build tools that turn customer .xlsx files into templates outside a browser.
Upgrade
v1.6 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.6 changelog, the images, selection, text rotation, form mode, page layout and PDF export docs for the full APIs, or try the images, multi-range selection, text rotation and form mode demos. The pricing matrix has the Lite vs Pro breakdown.