heysnap-web-viewers
v0.4.1
Published
Client-side React components for rendering PDF, DOCX, PPT, XLSX, images, video, audio, code, markdown, and HTML in the browser.
Maintainers
Readme
heysnap-web-viewers
Client-side React components for previewing documents and media in the browser. One toolbar vocabulary across the family — filename, format-specific controls, zoom, download — and aggressive optional-peer hygiene so a consumer only installs the engines for the viewers they actually use.
Ten viewers in the box:
| Format | Component | Subpath |
| ----------- | ----------------------- | ------------------------------ |
| PDF | HeySnapPdfViewer | heysnap-web-viewers/pdf |
| DOCX | HeySnapDocxViewer | heysnap-web-viewers/docx |
| PPTX | HeySnapPPTViewer | heysnap-web-viewers/ppt |
| XLSX | HeySnapXlsxViewer | heysnap-web-viewers/xlsx |
| Image | HeySnapImageViewer | heysnap-web-viewers/image |
| Video | HeySnapVideoViewer | heysnap-web-viewers/video |
| Audio | HeySnapAudioPlayer | heysnap-web-viewers/audio |
| Source code | HeySnapCodeViewer | heysnap-web-viewers/code |
| Markdown | HeySnapMarkdownViewer | heysnap-web-viewers/markdown |
| HTML | HeySnapHtmlViewer | heysnap-web-viewers/html |
Install
pnpm add heysnap-web-viewers react react-dom @hugeicons/react @hugeicons/core-free-iconsThat's the floor — React plus the icon set every toolbar uses. Each viewer's heavy engine deps are declared as optional peer dependencies: install only what you need.
| Viewer | Additional peers |
| ---------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| pdf | @embedpdf/core, @embedpdf/engines, @embedpdf/plugin-{document-manager,interaction-manager,pan,render,scroll,thumbnail,viewport,zoom} |
| docx | docx-preview, jszip |
| ppt | — (renders converted slide images; consumer supplies the manifest) |
| xlsx | @glideapps/glide-data-grid, @fontsource/geist-sans |
| image | — (uses the platform <img>) |
| video | — (uses the platform <video>) |
| audio | — (uses the platform <audio>) |
| code | monaco-editor, @monaco-editor/react |
| markdown | react-markdown, remark-gfm |
| html | — (renders into a sandboxed <iframe>) |
For example, a project using only PDF and code installs:
pnpm add heysnap-web-viewers react react-dom \
@hugeicons/react @hugeicons/core-free-icons \
@embedpdf/core @embedpdf/engines \
@embedpdf/plugin-document-manager @embedpdf/plugin-interaction-manager \
@embedpdf/plugin-pan @embedpdf/plugin-render @embedpdf/plugin-scroll \
@embedpdf/plugin-thumbnail @embedpdf/plugin-viewport @embedpdf/plugin-zoom \
monaco-editor @monaco-editor/reactpeerDependenciesMeta marks every engine as "optional": true, so npm/pnpm/yarn won't shout about the ones you skipped.
Stylesheet
The XLSX viewer ships a small stylesheet for its grid chrome. Import it once at the top level of your app if you use the XLSX viewer:
import "heysnap-web-viewers/style.css";Other viewers inject their styles themselves (markdown's preview rules; the rest use inline styles), so this import is a no-op for them.
Import
The package ships per-viewer subpath entries so bundlers tree-shake unused viewers to zero:
import { HeySnapPdfViewer } from "heysnap-web-viewers/pdf";
import { HeySnapCodeViewer } from "heysnap-web-viewers/code";
// or, for convenience, from the aggregate entry:
import { HeySnapPdfViewer, HeySnapCodeViewer } from "heysnap-web-viewers";Subpath imports are preferred — they let your bundler skip viewer chunks (and their optional peers) that aren't actually used.
Every viewer mounts at width: 100% / height: 100% and expects its parent to constrain its size.
HeySnapPdfViewer
A self-contained PDF viewer with a customizable toolbar (filename, cursor / hand-pan tools, zoom controls, download), an animated thumbnail sidebar, and full keyboard / screen-reader support.
import { useState } from "react";
import { HeySnapPdfViewer } from "heysnap-web-viewers/pdf";
export function App() {
const [file, setFile] = useState<File | null>(null);
return (
<>
<input
type="file"
accept="application/pdf"
onChange={(e) => setFile(e.target.files?.[0] ?? null)}
/>
{file && (
<div style={{ height: 600 }}>
<HeySnapPdfViewer src={file} />
</div>
)}
</>
);
}src accepts
- A URL string
- A
Filefrom an<input type="file"> - A
Blob - An
ArrayBufferorUint8Array
Notable props
| Prop | Type | Default | Notes |
| -------------------- | --------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| showHeader | boolean | true | Render the toolbar |
| headerBackground | string | "#fafafa" | Toolbar background |
| headerForeground | string | "#1f1f1f" | Toolbar foreground; drives every hover/active tint via currentColor + color-mix, so flipping this one prop swaps the whole chrome between light and dark |
| bodyBackground | string | "#e9eaed" | Gutter color around the rendered pages |
| defaultSidebarOpen | boolean | false | Whether the thumbnail sidebar starts open |
| sidebarBackground | string | "#f5f5f7" | Sidebar background |
| sidebarWidth | number | 180 | Sidebar width in px |
HeySnapDocxViewer
Read-only DOCX viewer with the same toolbar vocabulary as the PDF viewer. Built on docx-preview, with a compact custom header layered on top.
import { HeySnapDocxViewer } from "heysnap-web-viewers/docx";
<div style={{ height: 600 }}>
<HeySnapDocxViewer src={file} />
</div>;src accepts
URL string · File · Blob · ArrayBuffer · Uint8Array.
Notable props
| Prop | Type | Default | Notes |
| ------------------ | ---------------- | ----------- | --------------------------------------------------------------------------------------------------------------------- |
| headerForeground | string | "#1f1f1f" | Drives toolbar tints and the editor's internal --doc-text* tokens, so inner UI text stays readable in dark mode |
| bodyBackground | string | "#e9eaed" | Gutter color and the editor's --doc-bg token |
| showRuler | boolean | false | Horizontal page ruler |
| rulerUnit | "inch" \| "cm" | "cm" | Units when ruler is shown |
| showMarginGuides | boolean | false | Dotted page-margin guides |
| initialZoom | number | 1.0 | Startup zoom |
Set the three color props together — everything else cascades:
<HeySnapDocxViewer
src={file}
headerBackground="#1f242c"
headerForeground="#e6e8eb"
bodyBackground="#0b0e13"
/>HeySnapPPTViewer
Slide viewer that renders pre-converted slide images plus an optional thumbnail sidebar. The viewer doesn't convert .pptx itself — your backend (or HeySnapPPTConversion hooks) produces a SlideManifest, and the viewer renders it.
import { HeySnapPPTViewer, type SlideManifest } from "heysnap-web-viewers/ppt";
const manifest: SlideManifest = {
slides: [
{ src: "/slides/1.png", width: 1280, height: 720 },
{ src: "/slides/2.png", width: 1280, height: 720 },
],
name: "Deck.pptx",
};
<HeySnapPPTViewer src={manifest} />;src accepts
A SlideManifest object, or a URL string pointing at a JSON manifest of the same shape.
HeySnapXlsxViewer
Spreadsheet viewer built on @glideapps/glide-data-grid. Supports sheet tabs, formula bar, frozen panes, and the same zoom / theme vocabulary as the rest of the family.
Remember to import the stylesheet:
import "heysnap-web-viewers/style.css";
import { HeySnapXlsxViewer } from "heysnap-web-viewers/xlsx";<div style={{ height: 600 }}>
<HeySnapXlsxViewer src={workbook} />
</div>src accepts
A WorkbookJSON produced by your XLSX conversion pipeline. (The library doesn't parse .xlsx directly — pair this with a server-side converter or the heysnap-xlsxl CLI.)
HeySnapImageViewer
Lightweight image viewer with filename · zoom · download. Renders via the native <img>, so format support tracks whatever the browser supports (PNG / JPEG / GIF / WebP / AVIF / SVG / …) and there are no engine peers to install.
<HeySnapImageViewer src={file} />src accepts
URL string · data: URL · blob: URL · File · Blob · ArrayBuffer · Uint8Array. Object URLs are revoked automatically on src change and on unmount.
Notable props
| Prop | Type | Default | Notes |
| ---------------- | ---------------------- | ----------------- | ---------------------------------- |
| bodyBackground | string | "#e9eaed" | Gutter color around the image |
| imageStyle | CSSProperties | — | Merged onto the rendered <img> |
| alt | string | resolved filename | Image alt text |
| onError | (err: Error) => void | — | Called on resolve / decode failure |
Zoom is fit-to-window × user zoom: at 100 % the image fills the viewport without up-scaling past native pixels; + walks the preset ladder 25 / 50 / 75 / 100 / 125 / 150 / 200 / 300 / 400 / 600 / 800 %. The badge reads user zoom (not effective pixel scale).
HeySnapVideoViewer
Native <video> player with the family's title + download chrome. Codec/container support tracks the browser (MP4 H.264, WebM, Ogg, …).
<HeySnapVideoViewer src={file} controls autoPlay={false} muted={false} />src accepts
URL string · File · Blob · ArrayBuffer · Uint8Array.
Notable props
| Prop | Type | Default | Notes |
| ---------------- | -------------------------------- | ------------ | --------------------------------------------------- |
| controls | boolean | true | Native player controls |
| autoPlay | boolean | false | Start on load (most browsers require muted: true) |
| muted | boolean | false | Start muted |
| loop | boolean | false | Loop when finished |
| preload | "none" \| "metadata" \| "auto" | "metadata" | Preload behavior |
| poster | string | — | Poster image URL |
| bodyBackground | string | "#000000" | Letterbox color |
HeySnapAudioPlayer
Native <audio> player with the same chrome. Codec support tracks the browser (MP3, AAC, Opus, FLAC, WAV, …).
<HeySnapAudioPlayer src={file} />Same src shape and controls / autoPlay / muted / loop / preload props as the video viewer. The native pill is capped at 560 px wide so it doesn't sprawl in wide containers.
HeySnapCodeViewer
Read-only Monaco editor with filename · font-size · word-wrap · download in the header. Languages are inferred from the filename extension; consumers can override via the language prop.
import { HeySnapCodeViewer } from "heysnap-web-viewers/code";
<HeySnapCodeViewer src="/snippets/example.ts" />;src accepts
URL string · File · Blob · ArrayBuffer · Uint8Array. Buffers decode as UTF-8.
Notable props
| Prop | Type | Default | Notes |
| ------------------ | --------------------------------------------- | ---------------------- | ------------------------------------------------------------------------------------ |
| language | string | inferred from filename | Monaco language id ("typescript", "python", "plaintext", …) |
| theme | string | "heysnap-light" | Monaco theme id |
| initialFontSize | number | 12 | Font size in px (persisted to localStorage) |
| defaultWordWrap | "on" \| "off" | "off" | Soft-wrap initial state |
| wordWrap | "on" \| "off" | — | Controlled wrap state |
| onWordWrapChange | (next) => void | — | Wrap-toggle callback |
| options | editor.IStandaloneEditorConstructionOptions | — | Forwarded to Monaco; readOnly, fontSize, and wordWrap are locked by the viewer |
heysnap Monaco themes
The viewer ships two tight three-color themes that match the rest of the family in light and dark modes. They register automatically the first time the viewer mounts.
import { HEYSNAP_LIGHT_ID, HEYSNAP_DARK_ID } from "heysnap-web-viewers/code";
<HeySnapCodeViewer src={file} theme={darkMode ? HEYSNAP_DARK_ID : HEYSNAP_LIGHT_ID} />You can also register them on a Monaco instance that lives outside the viewer:
import { defineHeysnapThemes } from "heysnap-web-viewers/code";
defineHeysnapThemes(monaco); // pass your @monaco-editor/react `Monaco` instanceHeySnapMarkdownViewer
Renders Markdown via react-markdown + remark-gfm. The header carries a Preview / Code toggle next to the filename — Preview shows the rendered markdown, Code swaps in HeySnapCodeViewer on the same surface so the raw source reads as native chrome rather than a separate viewer. The header also surfaces zoom (preview) and word-wrap (code).
import { HeySnapMarkdownViewer } from "heysnap-web-viewers/markdown";
<HeySnapMarkdownViewer src="/docs/README.md" />
// or in-memory:
<HeySnapMarkdownViewer src={{ text: "# Hello", name: "hello.md" }} />src accepts
URL string · File · Blob · ArrayBuffer · Uint8Array · { text, name } for in-memory content.
Notable props
| Prop | Type | Default | Notes |
| ----------------------- | ------------------------------------- | ----------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| defaultMode | "preview" \| "code" | "preview" | Initial view mode |
| mode / onModeChange | — | — | Controlled mode pair |
| codeTheme | string | — | Monaco theme id forwarded to the embedded code view |
| initialZoom | number | 1.0 | Preview zoom (0.5–3× ladder) |
| bodyBackground | string | "#ffffff" | Background for both modes; match this to the heysnap Monaco bg (#FFFFFF light / #0F0F11 dark) so the toggle reads as a content swap |
| components | import("react-markdown").Components | — | Shallow-merged on top of the defaults; plug in syntax-highlighted code blocks, custom link wrappers, etc. |
GFM features (tables, task lists, strikethrough, autolinks) are on by default. The preview stylesheet is injected into document.head once on first mount — no CSS plumbing on the consumer side. All accent colors derive from currentColor + color-mix, so dark mode "just works" when the surrounding foreground is light.
HeySnapHtmlViewer
Renders HTML markup inside a sandboxed <iframe> (sandbox="allow-same-origin" by default — same-origin lets the viewer apply live zoom; no allow-scripts token means embedded JS is inert, which is the safe default for previewing untrusted markup). Same Preview / Code toggle and zoom / word-wrap chrome as the markdown viewer.
import { HeySnapHtmlViewer } from "heysnap-web-viewers/html";
<HeySnapHtmlViewer src={{ text: "<h1>Hi</h1>", name: "hi.html" }} />;src accepts
URL string · File · Blob · ArrayBuffer · Uint8Array · { text, name }.
Notable props
| Prop | Type | Default | Notes |
| --------------------------------------- | -------- | --------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| defaultMode / mode / onModeChange | — | — | Preview/Code mode (controlled or uncontrolled) |
| codeTheme | string | — | Monaco theme forwarded to the code view |
| initialZoom | number | 1.0 | Applied to the iframe's documentElement.style.zoom (no srcdoc re-render, scroll preserved) |
| sandbox | string | "allow-same-origin" | Override only if you trust the content. Adding allow-scripts enables JS inside the iframe — treat it as a trust decision |
| bodyBackground | string | "#ffffff" | Painted around the iframe; the iframe itself is transparent so this shows through |
Tree-shaking & bundle hygiene
- Subpath imports (
heysnap-web-viewers/pdf, …) are the canonical way to consume the package. Bundlers drop chunks for unused viewers entirely. sideEffectsis set to["**/*.css"]so CSS imports aren't shaken away. JS modules are pure.- Every viewer's heavy engine is an optional peer: install only what you use.
- The aggregate entry (
heysnap-web-viewers) is provided for convenience and will still tree-shake when your bundler does scope-hoisting; subpaths are the safer bet for minimum bundle size.
License
MIT © Ananya
