Getting started
This page sets up a PDF with continuous scrolling and built-in zoom gestures. Next, try Create your first annotation, Create your first keyword highlight, or Create your first custom annotation. Secure output from reviewed matches starts with keyword redaction.
Open the Viewer demo to see the completed minimal Viewer before writing the code below.
Provide two DOM elements and a PDF URL; Core creates and cleans up the document surface.
Requirements
- Node.js
^22.13.0or>=24.0.0 - A Vite or Webpack browser application
- A PDF URL accessible to the browser
Install
npm install @inklayer-dev/coreAdd the Viewer hosts
<div id="pdf-workspace">
<div id="pages"></div>
</div>Give the scroll container a real size. The rest of the application layout remains yours:
html, body, #pdf-workspace {
height: 100%;
margin: 0;
}
#pages {
height: 100%;
overflow: auto;
background: #f2f4f7;
}Load and display a PDF
import { createInkLayer } from '@inklayer-dev/core/capabilities'
import '@inklayer-dev/core/style'
const root = document.querySelector<HTMLElement>('#pdf-workspace')!
const pages = document.querySelector<HTMLElement>('#pages')!
const core = await createInkLayer({
root,
pageFlow: {
container: pages,
scale: 'page-width'
}
})
const documentHandle = await core.load({
url: '/documents/review.pdf',
range: 'auto'
})
console.log(`Opened ${documentHandle.numPages} pages`)You should now see a continuous PDF. pageFlow mounts and virtualizes page Canvas, TextLayer, and annotation surfaces inside #pages. range: 'auto' uses HTTP byte chunks for large files when the server supports them.
Core already includes a version-matched PDF.js Worker. Ordinary Vite and Webpack applications do not need to download, copy, or configure pdf.worker.
Override the Worker URL only when your Content Security Policy or deployment requires a self-hosted Worker:
const core = await createInkLayer({
root,
pageFlow: { container: pages },
viewer: { workerSrc: '/assets/pdf.worker.min.mjs' }
})Show loading progress and password UI
Core reports state; your application decides how it looks:
const stopViewer = core.viewer.subscribe(event => {
if (event.type === 'loadProgress') {
updateLoadingUI(event.progress)
}
if (event.type === 'passwordRequired') {
openPasswordDialog(event.request)
}
if (event.type === 'error') {
showDocumentError(event.error)
}
})See Load PDFs for local files, headers, password submission, progress, cancellation, and retry.
Clean up on unmount
async function unmount() {
stopViewer()
await core.destroy()
}When practical, await destroy() before reusing a host owned by Core. It releases the document, Worker resources, page surfaces, listeners, plugins, and pending work.