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:

  1. Mount the viewer inside a dedicated container within your app shell layout.
  2. Initialize the viewer with your chosen assets mode (useCDN: true or baseUrl).
  3. On document switch, unload the existing instance.
  4. Reload into the same container with the new document.
  5. 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
package.json
{
"name": "viewer-embedded-vanilla",
"private": true,
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"@nutrient-sdk/viewer": "latest"
},
"devDependencies": {
"vite": "latest"
}
}
index.html
<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>
src/main.js
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
package.json
{
"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"
}
}
src/App.jsx
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:

ModeUse forSetup
CDN (useCDN: true)Fastest integration and internet-accessible deploymentsConfigure useCDN: true in NutrientViewer.load(...).
Self-hosted (baseUrl)Compliance, offline, controlled infrastructureServe 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.

  • baseUrl points 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 (useCDN or baseUrl).
  • 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.