Miroff508 downloadsOpens local .html files as real rendered pages, with working CSS, images and video - zoom, bookmarks and scroll memory included, on desktop and mobile alike.
Opens local .html files in your vault as real rendered pages - CSS, images, and video all work, on desktop and mobile alike - instead of Obsidian refusing to display them at all.
Rendering - opening a .html file, zoom, page-native JS interactivity, following a link, opening and pinch/scroll-zooming an image, playing video in the in-app player.
Settings - pinning/unpinning toolbar actions, reordering them, resetting.
Viewing - Fit to width, playing video, following a link.
Bookmarks - opening the Bookmarks menu, jumping to a saved spot, managing bookmarks.
Obsidian doesn't render .html files - clicking one in the file explorer either does nothing useful or opens "unsupported file type." A common case: exporting a Telegram channel via Telegram Desktop produces messages.html plus css/, images/, js/, photos/, video_files/ - a real static page, just sitting in your vault, unreadable from inside Obsidian.
Two existing approaches were tried and ruled out during development, for concrete, observed reasons - not just guesswork:
iframe/srcdoc without inlining its CSS) run straight into Obsidian's own Content-Security-Policy, which restricts style-src to 'self' - an external stylesheet link inside that context gets silently blocked, so the page renders unstyled. Observed directly while comparing plugins during development: a real CSP violation in Obsidian's own console, naming exactly this.http://127.0.0.1:PORT and point an iframe at it) genuinely works around Obsidian's restrictions, but by its own documentation is desktop-only - it needs Node's http module, which doesn't exist in Obsidian's mobile (Capacitor) runtime at all. Telegram exports are exactly the kind of thing you'd want to read on your phone.Local Web Page Viewer takes a third path: no local server, no unstyled DOM injection. It reads the HTML, rewrites every relative resource reference (CSS, images, scripts) to a real vault resource path, inlines stylesheets as <style> text, and loads the result through iframe.srcdoc - which, unlike a plain iframe.src navigation, inherits the trusted origin needed to actually load those resources. No Node.js, no native code, no bundled browser engine - just the one every platform already has. The full mechanism, and why the alternatives above don't hold up, is in How it works.
Nobody else does all of this - most existing HTML viewers stop at "the text is visible."
| Local Web Page Viewer | "Inject into DOM" plugins | Local-HTTP-server plugins | |
|---|---|---|---|
| CSS renders correctly | ✅ | ❌ (blocked by Obsidian's CSP) | ✅ |
| Works on mobile | ✅ | partial at best | ❌ desktop-only |
| Video actually plays | ✅ (in-app player + codec fallback) | - | - |
| Zoom / fit-to-width | ✅ | ❌ | ❌ |
| Bookmarks inside a page | ✅ | ❌ | ❌ |
| Remembers scroll position | ✅ (per device) | ❌ | ❌ |
| Configurable toolbar | ✅ (per device) | ❌ | ❌ |
| No local server, no native code | ✅ | ✅ | ❌ |
url(), inline scripts - all load through the same mechanism Obsidian itself uses for resources embedded in notes, not a workaround that only handles the easy cases.Cmd +/- still controls the app as normal). Works on any page regardless of whether it has a responsive layout of its own: a fixed-width "desktop only" export gets shrunk to fit a phone, or stretched to fill a wide pane instead of sitting tiny in the middle of it. Built on transform: scale(), not the CSS zoom property - WebKit has a long-standing bug (webkit.org/b/77998) where zoom both reads and applies unreliably, confirmed live on-device during development.<video controls> player for video, pinch-to-zoom-and-pan for images (both mouse and touch).No bundled browser engine - each platform already has one, reached through a sandboxed <iframe>:
<webview> was tried first for extra isolation, but Obsidian's main window strips webviewTag support from any attached webview for security, leaving it unable to load anything - so a plain iframe is used instead.The file isn't just pointed at with iframe.src - Obsidian's main process only allows app:// resource requests whose requesting frame origin matches Obsidian's own trusted origin, and a src-navigated iframe gets a distinct origin of its own, so every subresource the page then requests (CSS, images, scripts) gets silently cancelled. A srcdoc document inherits its origin from the parent instead, which passes that check - so the plugin reads the HTML, rewrites every relative resource reference to an absolute vault resource path, inlines <link rel="stylesheet"> as <style> text (Obsidian's CSP allows inline styles but not cross-origin stylesheet links), and loads the result via iframe.srcdoc.
A link to another local file (a photo, a video) would normally navigate the iframe away from the rendered page entirely, with no address bar or back button to recover - so those clicks are intercepted and redirected to an in-app media viewer instead, loaded from the trusted top-level context rather than through the sandboxed frame.
Scripts are allowed (allow-scripts in the sandbox) - the trust level is the same as opening the file in a regular browser: it only runs what you already put in your own vault.
@import isn't inlined (only url(...) references are rewritten) - not needed for the Telegram-export case this was built for, but a page that splits its stylesheet across multiple @imported files will be missing those rules.messages.html to messages2.html) - it just loads whatever file is opened..html/.htm files in your vault - by design, not an oversight.Not yet in the official Community Plugins directory. Install via BRAT:
Once enabled, .html and .htm files open in this view automatically - Local Web Page Viewer registers itself as the handler for both extensions.
| Action | What it does |
|---|---|
| Reload | Re-renders the page from disk |
| Zoom in / Zoom out | Scales the page's own content, not Obsidian's interface |
| Reset zoom | Back to 100% |
| Fit to width | Scales a page with no responsive layout of its own to fit the pane (mobile) |
| Add bookmark | Click a spot on the page, name it, jump back later |
| Bookmarks | Jump to a saved bookmark, or manage/remove them |
| Open in system browser | Desktop only |
Which of these show as their own icon versus live in the "Page tools" menu, and in what order, is set independently for each device under Settings → Local Web Page Viewer.
Local dev tooling runs entirely in Docker - nothing is installed on the host beyond Docker itself.
docker compose build
docker compose run --rm build npm install
docker compose up -d # watches src/ and rebuilds main.js on change
Symlink the build output into a test vault's .obsidian/plugins/local-web-page-viewer/ folder, then reload plugins in Obsidian (Cmd+P → Reload app without saving) after each rebuild, or install the Hot Reload community plugin for automatic reloading.
docker compose run --rm build npm run lint # eslint-plugin-obsidianmd, same checks community.obsidian.md's scorecard runs
MIT