HTML Document Viewer is an Obsidian community plugin for viewing static .html
and .htm files stored in a vault. Each document is treated as untrusted and is
rendered read-only inside a separate sandboxed iframe.
The runtime implements milestones 1–9 from SPEC.md plus nested vault assets,
responsive images, CSS image URLs, and dependency-aware refresh. The plugin is published in the Obsidian
Community directory after passing its automated security, dependency,
provenance, and reproducible-build checks.
Install HTML Document Viewer from Settings → Community plugins → Browse.
Enable it, then select an .html or .htm file in the file explorer. The file
opens read-only in a normal Obsidian pane. It can be split into multiple panes,
and open views refresh when the document or one of its referenced assets
changes.
Document markup is never interpreted in Obsidian's DOM. The plugin parses HTML
in a detached document, removes active and navigation-producing content, inserts
a restrictive Content Security Policy (CSP), serializes with XMLSerializer,
and assigns only that prepared output to an iframe's srcdoc property.
The iframe has an empty sandbox attribute. It receives no allow-scripts,
allow-same-origin, navigation, popup, form, or download permissions. The CSP
also denies scripts, connections, frames, objects, workers, forms, media,
manifests, and every default resource type.
- Inline and external scripts do not run.
- Inline event handlers are removed.
- Forms and non-fragment navigation are disabled.
- HTTP, HTTPS, protocol-relative,
file:,app:, vault-escaping traversal, and other unsupported document-authored resource references are removed. - Supported vault raster bytes are rewritten to generated base64
data:URLs only after path normalization, vault lookup, magic-byte validation, and decoded-dimension limits. Authored raster data URLs receive the same byte and dimension validation. Stylesheet text is inserted into the detached document without HTML interpolation. - Documents are bounded before rendering: HTML source, DOM element count and depth, authored asset count, decoded image dimensions, individual image and stylesheet size, aggregate loaded bytes, and embedded output expansion all have fixed limits. Repeated references share one vault read, and superseded renders stop before further asset processing.
- Raster references in inline and linked CSS are resolved through the same vault-only loader. Unsupported or remote CSS URLs are replaced with empty URLs, and the CSP remains a second no-network boundary.
- The plugin makes no telemetry or network requests.
There is no trusted mode, bypass toggle, or setting that weakens this model.
The current build supports:
- ordinary static HTML;
- static presentation MathML, excluding
annotation-xmlintegration content; - inline CSS in
<style>andstyleattributes; - fragment-only links such as
href="#section"; - embedded base64 raster data images, including line-wrapped base64, using PNG, JPEG, GIF, WebP, or AVIF;
- relative PNG, JPEG, GIF, WebP, and AVIF images anywhere in the vault reachable by a relative path that does not escape the vault;
- responsive
srcsetand<picture>raster sources; - raster
url(...)references in inline styles and linked stylesheets; - relative
.cssstylesheets within the vault. Stylesheetmediaandtitlesemantics are preserved; disabled and alternate stylesheets remain inactive.
An open document refreshes after its source or an exact referenced dependency changes through a create, modification, deletion, or rename. Unrelated vault changes do not trigger work. Changes are debounced, stale asynchronous renders are discarded, and each pane owns its own refresh and prepared asset state. The same document can therefore remain open and live in more than one pane.
Disabling the plugin closes all open HTML Document Viewer panes. This blanks
their sandboxed frames, cancels pending refreshes, unregisters their vault
listeners, and discards prepared dependency state. After
re-enabling, opening an .html or .htm file creates a fresh view; stale tabs
are intentionally not left behind.
The specification originally preferred parent-created blob: URLs for local
images and a data:text/css URL for stylesheets. Current Obsidian applies an
inherited application CSP to srcdoc and gives an empty-sandbox frame an opaque
origin: the former blocks external data stylesheets, while the latter cannot
load a parent-origin blob:app://… URL. The runtime therefore uses allowlisted
base64 image data URLs and inline stylesheet text. This preserves the empty
sandbox, the no-network guarantee, and the vault-only validation boundary.
- The view is read-only.
- JavaScript, event handlers, WebAssembly, workers, embedded frames, objects, forms, downloads, popups, meta refresh, and top-level navigation are blocked.
- Remote resources and filesystem/application URLs are blocked.
- SVG, audio, video, fonts, and MathML
annotation-xmlare not supported. - CSS
@importand font URLs are not resolved. CSS image URLs support only the allowlisted raster formats. - HTML files are limited to 10 MiB and 5,000,000 decoded source characters, 100,000 elements, 256 levels of nesting, and 256 authored asset references. Local images are limited to 10 MiB each, 16,384 pixels in either dimension, and 40 million decoded pixels. Stylesheets are limited to 1 MiB each, total loaded assets to 25 MiB, and embedded asset output to 25 MiB.
- Settings and editing are not supported.
The runtime uses only Obsidian's Vault API and browser APIs. It does not use
Node.js, Electron, FileSystemAdapter, or absolute filesystem paths, and the
manifest supports both desktop and mobile Obsidian.
Desktop and mobile behavior have been verified in real Obsidian, including a
real-device mobile no-network observation and live same-folder stylesheet and
image refreshes. A clean desktop installation from the published release and a
clean mobile Community-directory installation have also passed. Current
evidence is recorded in
docs/testing.md.
Prerequisites are Node.js 22.22.0 or later, npm, Obsidian, and a dedicated test vault.
npm ci
npm run check
npm run install:local -- --vault /path/to/test-vaultThe install script refuses paths that are not existing Obsidian vaults and
copies only main.js, manifest.json, and styles.css into
<vault>/.obsidian/plugins/html-document-viewer/. Enable the plugin in
Settings → Community plugins, then open an .html or .htm file.
Available commands:
npm run dev
npm run build
npm run typecheck
npm run typecheck:min
npm run lint
npm test
npm run test:coverage
npm run test:e2e
npm run checkThe production bundle is generated as main.js and intentionally ignored by
Git. The release workflow attaches main.js, manifest.json, and styles.css
as separate attested assets. Maintainer configuration, retry behavior, first
release checks, and Community submission steps are documented in
docs/releasing.md.
Please follow SECURITY.md. Do not include exploit details or
private vault content in a public issue.
MIT © 2026 Tyson Gern.