Getting started

Get one file on screen, then choose how your app owns the UI. Try a file first in the playground; use the API reference when you need an exact option, event or data shape.

aared (عارض), published as @aared/viewer, previews PPTX, XLSX and DOCX locally in the browser; local files are not uploaded. It is view-only, not an editor or converter. Spreadsheet formulas use saved values, not recalculation. See fidelity and limits before relying on exact Office pagination or unsupported features.

Install

Install the published package in your app:

npm install @aared/viewer

For source development, see the repository’s development instructions.

Show your first file

The element is the shortest path to a viewer with controls. Import it once from your app’s code, which your bundler (Vite, webpack, Next.js) builds; bare package subpaths need a bundler or equivalent import map:

// main.ts
import "@aared/viewer/element";

Then point the element at a file and give it a height (see Sizing):

<aared-viewer src="quarterly-report.pptx" controls style="height: 600px"></aared-viewer>

It detects the format, opens the file, and draws it with built-in paging, zoom and a full-screen toggle. The element fetches src itself, so the file must be on your page’s origin or served with CORS headers.

To close this viewer, remove the element from the page. Changing src or setting its file property replaces the current file and cleans up the previous view.

Choose your integration

Choose how much UI and lifetime management your app owns. Every integration uses the same renderers, so choosing a simpler entry does not reduce fidelity.

You want to… Use
A viewer with built-in controls <aared-viewer controls>
A viewer in React, with app-owned UI usePreview() from @aared/viewer/react
A JavaScript viewer, with app-owned UI preview({ source, target }) from @aared/viewer/preview
Semantic data reads or manual document lifetime/rendering open() and render() from @aared/viewer/pptx, @aared/viewer/xlsx, or @aared/viewer/docx

Only accepting one format? Import preview() from its format entry instead of @aared/viewer/preview. Each format entry exports both the viewer and the manual document operations. React also offers usePptx(), useXlsx() and useDocx() for format-specific types.

Already showing the file? Keep using that viewer for controls and search. Its viewer.content provides semantic reads and inputs for secondary renders, such as thumbnails, without reopening the file. It does not expose document editing or rendering models. See multiple views and borrowed content.

The playground demonstrates HTML element, React, JavaScript viewer and Manual rendering. Start with the simplest one that meets your needs; data panels and thumbnails do not require replacing an existing viewer with manual document management.

Choose the source separately: all routes accept a URL string, File, Blob, ArrayBuffer or typed-array view. The element uses src for URLs and file for files/bytes; functions and hooks use source. A URL must be same-origin or CORS-enabled. For custom headers, use the authenticated fetch recipe.

Common tasks

Show any supported format from JavaScript

Use this for uploads that could be PowerPoint, Excel or Word. It opens a viewer without building a toolbar; the toolbar recipe adds your own controls:

import { preview } from "@aared/viewer/preview";

const viewer = await preview({ source: file, target: container });
console.log(viewer.format, viewer.info.count);
// When the feature unmounts: viewer.destroy();

You can also pass a URL as source. Detection and opening share the same download. Use detect() only when you need to inspect the type without opening a viewer (API reference).

Show one known format

Import the viewer from the format your app accepts. It has navigation, search, zoom and one cleanup call, while the other renderers stay out of your build:

import { preview } from "@aared/viewer/pptx";

const viewer = await preview({ source: file, target: container });
await viewer.next();
const matches = await viewer.search("budget");
// When the feature unmounts: viewer.destroy();

Change the import to @aared/viewer/xlsx or @aared/viewer/docx for those formats. The same object-form input returns their precisely typed controls. For an arbitrary file type, use @aared/viewer/preview. For semantic reads or thumbnails beside the viewer, use its viewer.content (below). See Single-format preview for sheet and page navigation examples.

Open a file the user picks

import "@aared/viewer/element";

const viewer = document.querySelector("aared-viewer")!;
const input = document.querySelector<HTMLInputElement>('input[type="file"]')!;
input.addEventListener("change", () => {
  // A newer pick replaces one still opening; clearing the input empties the
  // viewer unless the element has a `src` attribute, which then shows again.
  viewer.file = input.files?.[0] ?? null;
});

Fetch a file that needs a login

The element’s src sends no headers of yours. Fetch the file yourself and hand over the bytes (a Blob, ArrayBuffer or Uint8Array):

import "@aared/viewer/element";

const response = await fetch("/files/q3-report.xlsx", {
  headers: { Authorization: `Bearer ${sessionStorage.getItem("token") ?? ""}` },
});
if (!response.ok) throw new Error(`Download failed: ${response.status}`);
document.querySelector("aared-viewer")!.file = await response.blob();

Mount a viewer in React

The hook owns loading and cleanup. Keep the host mounted while displaying loading/error UI, and use status to access the ready viewer:

import { usePreview } from "@aared/viewer/react";
import { messageFor } from "@aared/viewer/core";

function Preview({ file }: { file: File }) {
  const { ref, status, viewer, index, error, retry } = usePreview({ source: file });
  return (
    <>
      <div ref={ref} style={{ height: 600 }} />
      {status === "loading" && <p>Loading...</p>}
      {status === "error" && <p>{messageFor(error)} <button onClick={retry}>Retry</button></p>}
      {status === "ready" && <p>{viewer.info.titles[index]}</p>}
    </>
  );
}

Use usePptx(), useXlsx() or useDocx() for a typed format handle. Keep the same file/bytes object across renders. These hooks mount the viewer and own its lifecycle; they do not draw the element’s built-in controls.

viewer.search() searches the document the viewer already has open (a workbook’s sheet on screen, as Excel’s Find does). It highlights matching text by default, including repeated DOCX headers. An empty search clears highlights; pass { highlight: false } for results only. The app supplies the input and results list; a native search input can provide its own clear button:

<label for="search">Search document</label>
<input id="search" type="search" placeholder="Search document">

This example jumps to the first match when the input fires change. For a results list, display the matches and navigate when a result is selected:

import { preview } from "@aared/viewer/preview";

const viewer = await preview({ source: file, target: container });
const box = document.querySelector<HTMLInputElement>("#search")!;
box.addEventListener("change", async () => {
  // Any format's result: its slide, its sheet and cell, or its place in a document.
  const [hit] = await viewer.search(box.value);
  if (hit) await viewer.goToSearchMatch(hit);
});

For live results, listen to input and pass an AbortSignal to cancel the previous search as the user types (Cancellation). Clearing the native input produces an input event; search its empty value to clear highlights. On XLSX, rerun the query after indexchange to refresh results for the new sheet. Highlights already follow the active query through navigation.

For a compact find bar, display viewer.searchNavigation.index + 1 and .count, and wire arrow buttons to .next() and .previous(). An inactive index is null; hide the counter or show “No results” when the count is zero. Listen to searchchange to update the counter. The active match gets a stronger outline and is revealed when stepping; all other matches stay softly highlighted. The API reference has a complete navigation example.

Follow your React theme

Pass the current appearance to the hook; it applies changes without reopening the file or moving the reader. Inline objects are compared by field values:

import { usePreview } from "@aared/viewer/react";

function Preview({ file, theme }: { file: File; theme: "light" | "dark" }) {
  const { ref } = usePreview({ source: file, appearance: { theme } });
  return <div ref={ref} style={{ height: 600 }} />;
}

The same behavior applies to usePptx(), useXlsx() and useDocx(), including spreadsheet appearance settings. index and zoom remain start values; use the viewer’s navigation and zoom methods to change them after mounting.

Build your own toolbar

The viewer’s toolbar methods are what <aared-viewer controls> uses itself; zoomIn(0.1) adds 10 points, and resetZoom() goes back to the zoom the format opens at:

import { preview } from "@aared/viewer/preview";
import { messageFor } from "@aared/viewer/core";

const viewer = await preview({ source: file, target: container });
const position = document.querySelector("#position")!;
const update = (): void => {
  position.textContent = `${viewer.index + 1} / ${viewer.info.count}`;
};
viewer.addEventListener("indexchange", update);
update();

const on = (selector: string, run: () => void): void => {
  document.querySelector(selector)!.addEventListener("click", run);
};
// App-owned actions report through their promises, not an error event.
const status = document.querySelector("#status")!;
const report = (error: unknown): void => { status.textContent = messageFor(error); };
on("#previous", () => void viewer.previous().catch(report));
on("#next", () => void viewer.next().catch(report));
on("#zoom-out", () => viewer.zoomOut(0.1));
on("#zoom-in", () => viewer.zoomIn(0.1));
on("#fit", () => viewer.resetZoom());

With open() and render() instead, paging is one render per slide into the same box, and you abort the one before yourself: the pattern is under Cancellation.

Show errors to people

Every failure is an AaredError with a code; messageFor() turns it into one plain sentence:

import { messageFor } from "@aared/viewer/core";
import "@aared/viewer/element";

const viewer = document.querySelector("aared-viewer")!;
const status = document.querySelector("#status")!;
viewer.addEventListener("load", () => {
  status.textContent = "";
});
viewer.addEventListener("error", (event) => {
  status.textContent = messageFor(event.detail.cause);
  console.error(event.detail.cause.code, event.detail.cause);
});

Events report automatic loads, attribute changes and built-in controls. When you call a method yourself, handle its promise instead; it does not also emit error. The same rule covers search and navigation. The codes and examples are in Errors.

Read cell data without a viewer

Use the low-level API when the job needs contents rather than viewer controls:

import { open } from "@aared/viewer/xlsx";

const workbook = await open(file);
try {
  const data = await workbook.sheets.get(0).read({ range: "A1:D20" });
  for (const row of data.rows) {
    for (const { col, cell } of row.cells) {
      console.log(row.index, col, cell.formatted);
    }
  }
} finally {
  workbook.close();
}

Rows are sparse { col, cell } entries. Values are those saved by Excel; formulas are never recalculated. slide.read() returns paragraphs and optional notes; DOCX sections expose page metadata in inches, with headings available separately. These are isolated data copies, not render inputs or formatting models. See reading data.

Render several slides from one document

For thumbnails beside the main view, render from the viewer’s borrowed content, viewer.content. It exposes reads/render inputs, not the document’s close() or session. Prepare one attached host per slide:

import { preview, render, type PptxRenderHandle } from "@aared/viewer/pptx";
import { messageFor } from "@aared/viewer/core";

const viewer = await preview({ source: file, target: container });
const strip = document.querySelector<HTMLElement>("#thumbnails")!;
const thumbnails: PptxRenderHandle[] = [];
for (const slide of viewer.content.slides.list()) {
  const host = document.createElement("button");
  host.style.width = "160px";
  host.onclick = () => void viewer.goTo(slide.info.index).catch((error: unknown) => {
    strip.title = messageFor(error);
  });
  strip.append(host);
  thumbnails.push(await render({ slide, target: host }));
}

// Call when the feature unmounts: thumbnails first, then the viewer (it closes the document).
function cleanup(): void {
  for (const thumbnail of thumbnails) thumbnail.destroy();
  viewer.destroy();
}

This opens once. For large decks, mount visible thumbnails on demand instead of painting all slides at startup. The same pattern works for sheets (viewer.content.sheets) and for range reads. DOCX can render a second view with render({ document: viewer.content, target }), including its images. Search stays on viewer.search(). To page through a document with no viewer at all, use open() + render(), where you own close().

Next tasks

You need Reference
File drop Set the element’s file to event.dataTransfer.files[0]; prevent default on dragover and drop
Sheet tabs, cell navigation, Word outline or pages Single-format preview, navigation
Media overrides or retained data Media, reading data
Data-only / workbook-wide search Searching a workbook, search grammar
Element CSS or spreadsheet chrome Styling, appearance
Cancel stale low-level renders Cancellation
CDN / single-file Worker setup Worker override
Deliver substitute fonts Sources and options

Sizing

A slide sizes itself: it takes the container’s width at the slide’s aspect ratio, and fit-page also fits a container’s height when it has one. Sheets and documents need the height from you: give the host a height, or put it in a scrolling box with a max-height. Without one, a sheet’s frozen rows, row numbers and column letters stay in view only while the grid scrolls itself; with no bounded box the page scrolls instead and they scroll away, and a document is as tall as its pages. <aared-viewer> is a flex column, so give it a height the same way (style="height: 600px").

Deployment

Vite and webpack

Nothing to configure: the bundler emits the Worker script beside your other assets, in dev servers and production builds alike. If your assets are served from a CDN on another origin, see Worker override.

Next.js

Every entry can be imported where the server prerenders; nothing touches the browser until it runs. Open and paint only in the browser, from a client component:

"use client";
import { usePreview } from "@aared/viewer/react";

export function Preview({ file }: { file: File }) {
  const { ref, status } = usePreview({ source: file });
  return <div ref={ref} style={{ height: 600 }} aria-busy={status === "loading"} />;
}

Troubleshooting

What you see Why What to do
A blank viewer, or one a few pixels tall Sheets and documents take their height from you Give the host a height (Sizing)
error with network-error src is on another origin without CORS headers, or the URL is wrong Serve it with CORS, or fetch it yourself and set file (recipe)
error with worker-error The browser didn’t start the Worker: your scripts are on a CDN (another origin), or a CSP blocks it Start it from a same-origin Blob: Worker override
error with encrypted-document The file is password-protected aared can’t open it; ask for an unprotected copy
error with unsupported-format The file isn’t a PowerPoint, Excel or Word file, or type names a different format Check the file, or drop type and let aared detect it
document or HTMLElement is not defined on the server Something opens or paints aared during server rendering Keep the import, but open and paint in the browser (Next.js)

A strict CSP needs no 'unsafe-inline' for aared’s styles or search highlights (Styling and CSP).