Vanilla JavaScript integration
Build a PDF Viewer without a UI framework. Start with the smallest working example, then add page navigation, thumbnails, an annotation toolbar, and a sidebar. For framework-specific examples, see Vue integration and React integration.
NOTE
Core renders PDF pages and their annotation layers. Your application provides the toolbar, sidebars, and layout.
Start with a minimal Viewer
Create a root element and an empty page container:
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>PDF Viewer</title>
</head>
<body>
<main id="pdf-workspace">
<div id="pages"></div>
</main>
<script type="module" src="/src/main.ts"></script>
</body>
</html>Give the page container a height and enable scrolling:
html, body, #pdf-workspace { height: 100%; margin: 0; }
#pages { height: 100%; overflow: auto; }Create Core and load the PDF:
import { createInkLayer } from '@inklayer-dev/core/capabilities'
import '@inklayer-dev/core/style'
import './style.css'
const core = await createInkLayer({
root: document.querySelector<HTMLElement>('#pdf-workspace')!,
pageFlow: {
container: document.querySelector<HTMLDivElement>('#pages')!,
scale: 'page-width'
}
})
await core.load({ url: '/documents/review.pdf' })Core renders the PDF pages inside #pages and handles continuous scrolling.
Add navigation and thumbnails
Replace the minimal layout with a toolbar and two sidebars. Keep #pages empty so Core can render into it:
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>PDF annotation workspace</title>
</head>
<body>
<main id="pdf-workspace" class="workspace">
<header class="toolbar">
<button id="previous">Previous</button>
<span id="page-number">1</span>
<button id="next">Next</button>
<button id="zoom-out">−</button>
<span id="zoom">100%</span>
<button id="zoom-in">+</button>
<button id="select">Select</button>
<button id="rectangle">Rectangle</button>
</header>
<aside id="thumbnails" class="sidebar"></aside>
<div id="pages" class="pages"></div>
<aside id="annotations" class="sidebar"></aside>
</main>
<script type="module" src="/src/main.ts"></script>
</body>
</html>html, body, #pdf-workspace { height: 100%; margin: 0; }
.workspace {
display: grid;
grid-template-columns: 160px minmax(0, 1fr) 220px;
grid-template-rows: auto minmax(0, 1fr);
}
.toolbar { grid-column: 1 / -1; }
.pages, .sidebar { overflow: auto; }
.sidebar img { max-width: 100%; }Build an annotation workspace
Connect the buttons, generate thumbnails, and subscribe to annotation changes. Clicking an annotation selects it and opens its page:
import { createInkLayer } from '@inklayer-dev/core/capabilities'
import '@inklayer-dev/core/style'
import './style.css'
const root = document.querySelector<HTMLElement>('#pdf-workspace')!
const pages = document.querySelector<HTMLDivElement>('#pages')!
const thumbnailList = document.querySelector<HTMLElement>('#thumbnails')!
const annotationList = document.querySelector<HTMLElement>('#annotations')!
const pageNumber = document.querySelector<HTMLElement>('#page-number')!
const zoom = document.querySelector<HTMLElement>('#zoom')!
const thumbnailUrls: string[] = []
let currentPage = 0
const core = await createInkLayer({
root,
pageFlow: {
container: pages,
scale: 'page-width',
onCurrentPageChanged(pageIndex) {
currentPage = pageIndex
pageNumber.textContent = String(pageIndex + 1)
},
onScaleChanged(state) {
zoom.textContent = `${state.percentage}%`
}
}
})
const pdf = await core.load({ url: '/documents/review.pdf' })
const pageFlow = core.getPageFlow()!
document.querySelector<HTMLButtonElement>('#previous')!.onclick = () => {
pageFlow.scrollToPage(Math.max(0, currentPage - 1))
}
document.querySelector<HTMLButtonElement>('#next')!.onclick = () => {
pageFlow.scrollToPage(Math.min(pdf.numPages - 1, currentPage + 1))
}
document.querySelector<HTMLButtonElement>('#zoom-out')!.onclick = () => {
void pageFlow.zoomOut()
}
document.querySelector<HTMLButtonElement>('#zoom-in')!.onclick = () => {
void pageFlow.zoomIn()
}
document.querySelector<HTMLButtonElement>('#select')!.onclick = () => {
core.annotations.setTool('select')
}
document.querySelector<HTMLButtonElement>('#rectangle')!.onclick = () => {
core.annotations.setTool('rectangle')
}
for (let pageIndex = 0; pageIndex < pdf.numPages; pageIndex++) {
const thumbnail = await core.viewer.renderThumbnail({ pageIndex, maxWidth: 140 })
const url = URL.createObjectURL(thumbnail.blob)
thumbnailUrls.push(url)
const button = document.createElement('button')
const image = document.createElement('img')
image.src = url
image.alt = `Page ${pageIndex + 1}`
button.append(image)
button.onclick = () => pageFlow.scrollToPage(pageIndex)
thumbnailList.append(button)
}
function updateAnnotations(): void {
annotationList.replaceChildren()
for (const annotation of core.annotations.repository.getAll()) {
const button = document.createElement('button')
button.textContent = `${annotation.type} · Page ${annotation.pageIndex + 1}`
button.onclick = () => {
core.annotations.setSelection({ ids: [annotation.id] })
pageFlow.scrollToPage(annotation.pageIndex)
}
annotationList.append(button)
}
}
const stop = core.annotations.repository.subscribe(updateAnnotations)
export async function destroyPdfWorkspace(): Promise<void> {
stop()
for (const url of thumbnailUrls) URL.revokeObjectURL(url)
await core.destroy()
}Call destroyPdfWorkspace() when the workspace is removed. For text highlights, author permissions, and saving annotations, see Create your first annotation and Persist annotations.