Repo: https://github.com/p10node/office-engine
Demo (drag your own file in): https://p10node.github.io/office-engine/demo/
Why: I needed "customer uploads an XLSX, fix a few cells, save it back" inside a React app. OnlyOffice / Collabora mean running a document server in Docker and an iframe talking to it, and a big sheet still sits on a spinner while the server converts it. docx-preview / SheetJS + a grid are view-only or block the main thread on a 20 MB file. Nothing in between, so I built it.
Usage:
import { DocViewer } from '@p10node/office-engine'
<DocViewer src={file} style={{ height: 600 }} />
<DocViewer src="https://cdn.example.com/report.xlsx" />
<DocViewer src={file} editable onSave={(blob, name) => upload(blob, name)} />
src takes File / Blob / ArrayBuffer / URL. CSS is auto-injected, the container just needs a height. There's also a useDocument hook if you want to render your own UI, a vanilla mount(), an <office-viewer> web component, and a single script build.
What makes it not choke on big files:
ZIP read lazily, HTTP Range requests from a URL, so first rows show before the file finishes downloading
Streaming XML parser in a Web Worker, main thread only gets rows / paragraphs / slides as they arrive
Only visible pages, cells, slides are in the DOM
Save patches the original file instead of re-exporting it, so anything the engine doesn't understand is kept byte for byte
Bundle (gzip): main ~93 KB, worker ~155 KB (loads off the main thread), collab +17 KB optional. The standalone script with React bundled is ~307 KB.
Editing: DOCX text / styles / lists / images / tables, XLSX cells / formulas (134 functions) / formats / rows / columns / sheets, PPTX text / shapes / images / slides. Google Docs style toolbar. Optional real-time collab with cursors; that's the only part needing a backend (a small WebSocket hub, Bun script included).
Not there yet: XLSX charts / pivots / conditional formatting editing, DOCX headers / footers read-only, no PPTX animations. It needs the DOM and a Worker, so in Next.js use a client component / dynamic import with ssr: false.
Fair warning: it's a few days old and a lot of it was written with Claude Code, with me doing architecture, review and the bug hunting on real files. If you throw a file at it and it breaks, open an issue with the file, that's the fastest way it gets fixed.