Embed Web SDK in a dashboard/app shell
Use this guide when integrating Nutrient Web SDK into a larger dashboard layout with persistent app controls and document switching.
Canonical embedding flow
The recommended integration flow for embedding the viewer in an app shell with document switching is as follows:
- Mount the viewer inside a dedicated container within your app shell layout.
- Initialize the viewer with your chosen assets mode (
useCDN: trueorbaseUrl). - On document switch, unload the existing instance.
- Reload into the same container with the new document.
- Restore minimal state (for example, page index/zoom) when appropriate.
App shell layout embedding
The viewer can be embedded in any container within your app shell. The example below shows a common dashboard layout with a header, sidebar, and main content area where the viewer is mounted.
<div class="app-shell"> <header>Global app header</header> <aside>Persistent app navigation</aside> <main> <div id="viewer" style="height: calc(100vh - 64px)"></div> </main></div>In-place document switching with lifecycle safety
To switch documents without remounting the viewer container, call NutrientViewer.unload(container) before loading the new document. This ensures proper cleanup of the previous instance and prevents memory leaks:
import NutrientViewer from "@nutrient-sdk/viewer";
const container = document.getElementById("viewer");
if (!container) { throw new Error('Viewer container "#viewer" not found.');}
let instance = null;let lastViewState = null;
async function loadDocument(documentUrl) { if (instance) { lastViewState = { currentPageIndex: instance.viewState.currentPageIndex, zoom: instance.viewState.zoom };
NutrientViewer.unload(container); instance = null; }
instance = await NutrientViewer.load({ container, document: documentUrl, useCDN: true, initialViewState: new NutrientViewer.ViewState({ currentPageIndex: lastViewState?.currentPageIndex ?? 0, zoom: lastViewState?.zoom ?? 1 }) });
return instance;}Runnable reference project
The following project layouts show a minimal end-to-end setup you can adapt as a starting point. Each variant produces the same runtime behavior, so pick the one that matches your stack.
Variant A — Vanilla (Vite)
viewer-embedded-vanilla/├─ package.json├─ index.html├─ src/main.js└─ public/ ├─ sample.pdf └─ Document.pdf{ "name": "viewer-embedded-vanilla", "private": true, "scripts": { "dev": "vite", "build": "vite build", "preview": "vite preview" }, "dependencies": { "@nutrient-sdk/viewer": "latest" }, "devDependencies": { "vite": "latest" }}<div> <button id="docA">Open A</button> <button id="docB">Open B</button></div><div id="viewer" style="height: 100vh"></div><script type="module" src="/src/main.js"></script>import NutrientViewer from "@nutrient-sdk/viewer";
const container = document.querySelector("#viewer");
if (!container) { throw new Error('Viewer container "#viewer" not found.');}
let currentInstance;
async function mount(documentPath) { if (currentInstance) { NutrientViewer.unload(container); currentInstance = null; }
currentInstance = await NutrientViewer.load({ container, document: documentPath, useCDN: true });}
await mount("/sample.pdf");
document .querySelector("#docA") ?.addEventListener("click", () => mount("/sample.pdf"));document .querySelector("#docB") ?.addEventListener("click", () => mount("/Document.pdf"));Variant B — React
viewer-embedded-react/├─ package.json├─ src/main.jsx├─ src/App.jsx└─ public/sample.pdf{ "name": "viewer-embedded-react", "private": true, "scripts": { "dev": "vite", "build": "vite build", "preview": "vite preview" }, "dependencies": { "@nutrient-sdk/viewer": "latest", "react": "latest", "react-dom": "latest" }, "devDependencies": { "@vitejs/plugin-react": "latest", "vite": "latest" }}import { useEffect, useRef } from "react";import NutrientViewer from "@nutrient-sdk/viewer";
export default function App() { const containerRef = useRef(null);
useEffect(() => { const container = containerRef.current;
if (!container) { throw new Error("Viewer container is not available."); }
let instance = null; let cancelled = false;
(async () => { instance = await NutrientViewer.load({ container, document: "/sample.pdf", useCDN: true });
if (cancelled && instance) { NutrientViewer.unload(container); instance = null; } })();
return () => { cancelled = true;
if (instance) { NutrientViewer.unload(container); } }; }, []);
return <div ref={containerRef} style={{ height: "100vh" }} />;}Assets setup decision
The table below summarizes the two asset setup modes for Web SDK and what each is for:
| Mode | Use for | Setup |
|---|---|---|
CDN (useCDN: true) | Fastest integration and internet-accessible deployments | Configure useCDN: true in NutrientViewer.load(...). |
Self-hosted (baseUrl) | Compliance, offline, controlled infrastructure | Serve the full assets archive from your domain and configure baseUrl. |
Required files — For baseUrl mode, the exact required runtime set is the entire contents of the downloaded assets archive, served with the original folder structure preserved.
Common 404 checks
Use the checks below to identify the most common asset-loading misconfigurations.
baseUrlpoints to the actual public URL root where assets are served.- Asset archive contents were extracted completely (no nested extra folder level).
- Development/proxy/CDN configuration doesn’t rewrite asset requests away from the asset root.
- Network tab shows runtime assets loaded from expected host/path.
Integration checklist
Use the checklist below to validate your setup before moving to production.
- Verify asset mode/path (
useCDNorbaseUrl). - Confirm entrypoint import and viewer mount container.
- Unload before reload when switching documents.
- Reapply toolbar/custom UI configuration after each load.
- Confirm export behavior matches your workflow.
Related guides
- Open and display PDFs in the browser using JavaScript
- Self-host assets in Web SDK
- Create a custom toolbar in our JavaScript PDF viewer