npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@virtualansoftware/vs-office-viewer

v1.0.3

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 a filename hint
  • 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-viewer
yarn add office-viewer
pnpm add office-viewer

Peer 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 though docx-preview does 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:

  • ResizeObserver
  • ArrayBuffer / 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