office-viewer-react
v1.0.2
Published
A zero-configuration React component for rendering Word, Excel, and PowerPoint files directly in the browser. No server required — everything processes locally.
Readme
office-viewer
A zero-configuration React component for rendering Word, Excel, and PowerPoint files directly in the browser. No server required — everything processes locally.
Features
| Format | Capability | |--------|-----------| | Word (.docx) | Text, tables, images, headers/footers, continuous scroll and page-by-page navigation, DrawingML charts rendered to PNG via ECharts | | Excel (.xlsx) | Cell styles and colours, merged cells, date and number formatting, formula values, multi-sheet tab bar with overflow scroll, sticky row and column headers | | PowerPoint (.pptx) | All slide layouts, images, text, shapes, vertical scroll and slide-by-slide navigation with keyboard arrow key support |
- Zero-config CSS — styles are auto-injected at runtime; no separate CSS import required
- Automatic format detection — determines the file type from
File.name,Blob.type, or afilenamehint - Built-in toolbar — view-mode selector (Continuous Scroll / Page Navigation) rendered internally for Word and PowerPoint; consumers own zero UI state
- SSR-safe — style injection is guarded by
typeof document !== "undefined"
Installation
npm install office-vieweryarn add office-viewerpnpm add office-viewerPeer dependencies
The following packages must already be present in your project:
npm install react react-dom lucide-react| Peer | Version |
|------|---------|
| react | ^18.0.0 \|\| ^19.0.0 |
| react-dom | ^18.0.0 \|\| ^19.0.0 |
| lucide-react | >=0.400.0 |
Quick start
import { OfficeViewer } from "office-viewer";
export default function App() {
const [file, setFile] = React.useState<File | null>(null);
return (
<>
<input
type="file"
accept=".docx,.xlsx,.pptx"
onChange={(e) => setFile(e.target.files?.[0] ?? null)}
/>
{file && (
<div style={{ height: "100vh" }}>
<OfficeViewer file={file} />
</div>
)}
</>
);
}No CSS import. No state for view mode or pagination. No configuration.
Usage
From a file input (File)
import { OfficeViewer } from "office-viewer";
function FileViewer() {
const [file, setFile] = React.useState<File | null>(null);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setFile(e.target.files?.[0] ?? null);
};
return (
<div style={{ height: "100vh", display: "flex", flexDirection: "column" }}>
<input type="file" accept=".docx,.xlsx,.pptx" onChange={handleChange} />
{file && <OfficeViewer file={file} style={{ flex: 1 }} />}
</div>
);
}From a Blob (e.g. a fetch response)
import { OfficeViewer } from "office-viewer";
function RemoteViewer({ url, filename }: { url: string; filename: string }) {
const [blob, setBlob] = React.useState<Blob | null>(null);
React.useEffect(() => {
fetch(url)
.then((r) => r.blob())
.then(setBlob);
}, [url]);
if (!blob) return <p>Loading…</p>;
return (
<div style={{ height: "80vh" }}>
{/* Pass filename so the component can detect .docx / .xlsx / .pptx */}
<OfficeViewer file={blob} filename={filename} />
</div>
);
}From an ArrayBuffer
import { OfficeViewer } from "office-viewer";
function BufferViewer({
buffer,
filename,
}: {
buffer: ArrayBuffer;
filename: string;
}) {
return (
<div style={{ height: "80vh" }}>
<OfficeViewer file={buffer} filename={filename} />
</div>
);
}Error handling
import { OfficeViewer } from "office-viewer";
function SafeViewer({ file }: { file: File }) {
const handleError = (err: Error) => {
console.error("Viewer error:", err.message);
// show a toast, report to Sentry, etc.
};
return (
<div style={{ height: "80vh" }}>
<OfficeViewer file={file} onError={handleError} />
</div>
);
}Custom container styling
Pass a className to size or position the viewer wrapper:
<OfficeViewer file={file} className="h-screen w-full rounded-xl shadow-lg" />API
<OfficeViewer />
The only public component. All internal state (view mode, page/slide index, sheet selection) is managed inside the component.
import { OfficeViewer, type OfficeViewerProps } from "office-viewer";Props
| Prop | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| file | File \| Blob \| ArrayBuffer | Yes | — | Document to render. The type is detected automatically. |
| filename | string | No | "" | Filename hint for type detection when passing a bare Blob or ArrayBuffer that carries no name. Example: "report.xlsx" |
| className | string | No | "" | CSS class applied to the outermost wrapper <div> for sizing and positioning. |
| onError | (e: Error) => void | No | — | Called when the document fails to load or render. The component also displays an inline error message. |
File type detection rules
| Input type | How the type is resolved |
|---|---|
| File | file.name extension (.docx, .xlsx, .pptx) |
| Blob with MIME type | blob.type — checks for wordprocessingml, spreadsheetml, presentationml |
| Blob without MIME type | filename prop extension |
| ArrayBuffer | filename prop extension |
If the type cannot be determined, an "Unsupported file type" message is shown.
TypeScript
The OfficeViewerProps interface is exported for use in wrapper components:
import type { OfficeViewerProps } from "office-viewer";
interface MyViewerProps extends Omit<OfficeViewerProps, "onError"> {
onRenderError?: (err: Error) => void;
}Format details
Word (.docx)
- Rendered by docx-preview
- DrawingML chart elements are pre-processed before rendering: chart XML is parsed, converted to an ECharts option, rendered off-screen to a PNG, and inlined as an
<img>in the document — so charts display correctly even thoughdocx-previewdoes not natively support them - Built-in toolbar provides two view modes:
- Continuous Scroll — all pages flow vertically
- Page Navigation — one page at a time with prev/next buttons and a page counter
Excel (.xlsx)
- Dual-parser strategy: ExcelJS supplies cell styles, fonts, and colours; SheetJS supplies formula-evaluated values
- Multi-sheet support — tab bar at the bottom with left/right scroll arrows for wide workbooks
- Sticky first row (header) and first column
- Merged cell rendering
- Date, number, currency, and percentage formatting
- Bold, italic, text colour, and background colour fidelity
PowerPoint (.pptx)
- Rendered by pptx-preview
- Slides re-render automatically when the container resizes (250 ms debounce via
ResizeObserver) - Built-in toolbar provides two view modes:
- Vertical Scroll — all slides flow vertically
- Slide Navigation — one slide at a time with prev/next buttons and a slide counter
- Keyboard navigation in Slide Navigation mode:
←/↑for previous,→/↓for next
Sizing
The component uses width: 100% and flex: 1 internally. Give the wrapper a defined height so scroll and navigation modes work correctly:
{/* Full-page viewer */}
<div style={{ height: "100vh" }}>
<OfficeViewer file={file} />
</div>
{/* Panel inside a layout */}
<div style={{ height: "calc(100vh - 64px)" }}>
<OfficeViewer file={file} />
</div>
{/* With Tailwind CSS */}
<OfficeViewer file={file} className="h-screen" />Note: A defined height is especially important for PowerPoint (Slide Navigation mode) and Excel (the grid fills the available space with sticky headers).
Browser support
| Browser | Version | |---------|---------| | Chrome | 90+ | | Edge | 90+ | | Firefox | 90+ | | Safari | 14+ |
Requires support for:
ResizeObserverArrayBuffer/File.arrayBuffer()- CSS custom properties
oklch()color function (Chrome 111+, Firefox 113+, Safari 15.4+) — used for design tokens; falls back gracefully in older browsers
Examples
Next.js (App Router)
// app/viewer/page.tsx
"use client";
import { useState } from "react";
import { OfficeViewer } from "office-viewer";
export default function ViewerPage() {
const [file, setFile] = useState<File | null>(null);
return (
<div className="flex h-screen flex-col">
<header className="flex h-16 items-center px-6 border-b">
<input
type="file"
accept=".docx,.xlsx,.pptx"
onChange={(e) => setFile(e.target.files?.[0] ?? null)}
/>
</header>
<main className="flex-1 overflow-hidden">
{file && <OfficeViewer file={file} className="h-full" />}
</main>
</div>
);
}Vite + React
// src/App.tsx
import { useState } from "react";
import { OfficeViewer } from "office-viewer";
export default function App() {
const [file, setFile] = useState<File | null>(null);
return (
<div style={{ height: "100vh", display: "flex", flexDirection: "column" }}>
<div style={{ padding: "1rem", borderBottom: "1px solid #e2e8f0" }}>
<input
type="file"
accept=".docx,.xlsx,.pptx"
onChange={(e) => setFile(e.target.files?.[0] ?? null)}
/>
</div>
<div style={{ flex: 1, overflow: "hidden" }}>
{file && <OfficeViewer file={file} />}
</div>
</div>
);
}Fetching a document from an API
import { useEffect, useState } from "react";
import { OfficeViewer } from "office-viewer";
function DocumentViewer({ documentId }: { documentId: string }) {
const [buffer, setBuffer] = useState<ArrayBuffer | null>(null);
const [filename, setFilename] = useState("");
const [loading, setLoading] = useState(true);
useEffect(() => {
setLoading(true);
fetch(`/api/documents/${documentId}`)
.then(async (res) => {
// Read filename from Content-Disposition header if present
const disposition = res.headers.get("Content-Disposition") ?? "";
const match = disposition.match(/filename="?([^"]+)"?/);
if (match) setFilename(match[1]);
return res.arrayBuffer();
})
.then((ab) => { setBuffer(ab); setLoading(false); });
}, [documentId]);
if (loading) return <p>Loading document…</p>;
if (!buffer) return <p>Failed to load document.</p>;
return (
<div style={{ height: "80vh" }}>
<OfficeViewer file={buffer} filename={filename} />
</div>
);
}License
MIT © Virtualan Software
