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.
Add a search box
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).