How to Build a Custom PDF Viewer Web Component in JavaScript with Stencil.js
Dynamic Web TWAIN is an SDK which enables document scanning and viewing from browsers. It can display PDF files in a customizable viewer.
In this article, we are going to build a web component with Stencil.js to wrap Dynamic Web TWAIN as a PDF viewer so that we can embed a PDF file in an HTML website easily using the following code:
<pdf-viewer
url="https://www.dynamsoft.com/codepool/assets/patch-code-sample.pdf"
></pdf-viewer>
The following is the PDF viewer embedded in this blog.
Note: The code in this article is no longer maintained. Please use the new Dynamsoft Document Viewer instead.
What you’ll build: A reusable <pdf-viewer> web component using Stencil.js and Dynamic Web TWAIN that embeds a fully interactive PDF viewer — with zoom, page navigation, thumbnail sidebar, fullscreen, and document scanning — into any HTML page with a single tag.
Key Takeaways
- Stencil.js web components let you wrap complex PDF rendering logic into a single
<pdf-viewer>custom element that works in any framework or plain HTML. - Dynamic Web TWAIN provides the PDF rendering engine, thumbnail viewer, and document scanning capabilities through its JavaScript API.
- The component supports zoom controls, fit-to-window, page navigation, fullscreen mode, local file loading, and scanner integration out of the box.
- ShadowDOM encapsulation requires using
CreateDWTObjectExinstead of the standard initialization method.
Common Developer Questions
- How do I embed a PDF viewer in an HTML page using a web component?
- How do I build a custom JavaScript PDF viewer with zoom, thumbnails, and page navigation?
- How do I use Dynamic Web TWAIN inside a Stencil.js web component with ShadowDOM?
How to Build a PDF Viewer Web Component with Stencil.js
Let’s build the component in steps.
Prerequisites
- Node.js installed on your machine
- Basic familiarity with TypeScript and JSX
- Get a 30-day free trial license for Dynamic Web TWAIN
Step 1: Create a New Stencil.js Project
Run the following command to create a new Stencil Component project:
npm init stencil
Step 2: Generate the PDF Viewer Component
Generate a new component named pdf-viewer.
npx stencil g pdf-viewer
To test the component, we can modify the src\index.html file:
- <my-component first="Stencil" last="'Don't call me a framework' JS"></my-component>
+ <pdf-viewer>
+ <p>Inner elements</p>
+ </pdf-viewer>
Then run the following command to test it:
npm run start
Step 3: Install the Dynamic Web TWAIN SDK
Run the following to install Dynamic Web TWAIN.
npm install dwt@18.0.0
Step 4: Initialize Dynamic Web TWAIN in the Component
Next, we are going to initialize an instance of Dynamic Web TWAIN and bind the viewer to a container.
-
In the render function, add a container.
containerID:string = "dwtcontrolContainer"; container:HTMLDivElement; render() { return ( <Host> <div id={this.containerID} class="container" ref={(el) => this.container = el as HTMLDivElement}> </div> <slot></slot> </Host> ); } -
In the
componentDidLoadlifecycle event, initialize Dynamic Web TWAIN.DWObject:WebTwain; componentDidLoad() { this.initDWT(); } initDWT() { Dynamsoft.DWT.ResourcesPath = "https://unpkg.com/dwt@18.0.0/dist"; let pThis = this; Dynamsoft.DWT.RegisterEvent('OnWebTwainReady', () => { Dynamsoft.DWT.CreateDWTObjectEx( { WebTwainId: 'dwtcontrol' }, function(obj) { pThis.DWObject = obj; pThis.DWObject.Viewer.bind(pThis.container); pThis.DWObject.Viewer.show(); }, function(err) { console.log(err); } ); }); Dynamsoft.DWT.Containers = [{ WebTwainId: 'dwtObject' }]; Dynamsoft.DWT.Load(); }CSS:
.container { position: relative; }Because web component uses shadowDom, we need to use the
CreateDWTObjectExfunction to create a new Web TWAIN object. -
Add a license prop to set the license for Dynamic Web TWAIN. You can apply for a license here.
@Prop() license?: string; initDWT() { if (this.license) { Dynamsoft.DWT.ProductKey = this.license; } }
Step 5: Add Props to Control the PDF Viewer
-
Create a thumbnail viewer and add a prop to set whether to enable it by default.
thumbnailShown:boolean = true; initDWT() { //... pThis.thumbnailViewer = pThis.DWObject.Viewer.createThumbnailViewer(); if (pThis.showthumbnailviewer === "true") { pThis.thumbnailViewer.show(); pThis.thumbnailShown = true; }else{ pThis.thumbnailShown = false; } //... } -
Add a
urlprop. If it is set, load the PDF file via the URL after the component is mounted.@State() status: string = ""; async loadPDF() { if (this.url) { this.status = "Loading PDF..."; try { let response = await fetch(this.url); let blob = await response.blob(); let pThis = this; this.DWObject.LoadImageFromBinary(blob,function(){ pThis.DWObject.Viewer.singlePageMode=true; pThis.DWObject.SelectImages([0]); },function(){}); this.status = ""; } catch (error) { this.status = "Failed to load the PDF"; } } }A status container is used to indicate the status.
JSX:
renderStatus(){ if (this.status) { return (<div class="status">{this.status}</div>); } } render() { return ( <Host> <div id={this.containerID} class="container" ref={(el) => this.container = el as HTMLDivElement}> {this.renderStatus()} </div> <slot></slot> </Host> ) }CSS:
.status { position: absolute; left: 0; top: 0; z-index: 99; } -
Now, we can update
index.htmlto use the PDF viewer to load a PDF file.<div class="app" style="width:800px;height:600px;"> <pdf-viewer style="width:100%;height:100%;" showthumbnailviewer="true" url="./sample.pdf"></pdf-viewer> </div>

Step 6: Add a Toolbar with Zoom, Navigation, and Actions
Next, we can add a toolbar for the viewer. It has the following functions:
- Toggle the thumbnail viewer.
- Display the current page number and total page number.
- Set the display percentage (zoom).
- Scan documents.
- Load local PDF or image files.
- Save the document as a PDF file.
- Enter fullscreen.
Add the toolbar in JSX (the toolbar and the viewer of Dynamic Web TWAIN are wrapped in a parent container):
render() {
return (
<Host>
<div class="container" ref={(el) => this.parentContainer = el as HTMLDivElement}>
<div class="toolbar">
</div>
<div class="viewer-container">
<div id={this.containerID} class="dwt-container" ref={(el) => this.dwtContainer = el as HTMLDivElement}>
{this.renderStatus()}
</div>
</div>
</div>
<slot></slot>
</Host>
);
}
CSS:
.toolbar {
height: 30px;
border: 1px solid rgb(204, 204, 204);
display: flex;
align-items: center;
background: white;
}
.container {
position: relative;
width: 100%;
height: 100%;
}
.viewer-container {
position: relative;
width: 100%;
height: calc(100% - 30px);
}
.dwt-container {
width: 100%;
height: 100%;
}
Add a Thumbnail Viewer Toggle Button
-
Add the button in JSX.
<div class="toolbar"> <div class="toolbar-item"> <img title="Toggle thumbnail viewer" class="Icon" src={sidebar} onClick={()=>this.toggleThumbnailViewer()}/> </div> </div>CSS:
.toolbar-item { display: flex; align-items: center; height: 100%; padding-right: 5px; } .Icon { width: 22px; height: 22px; cursor: pointer; } -
Convert the icon’s SVG file to base64 and save it in the component’s
assets/base64.tsxfile so that we can use it in the component.export const sidebar = "data:image/svg+xml;base64,PD94b..."; -
The
toggleThumbnailViewerfunction.@Method() async toggleThumbnailViewer() { if (this.thumbnailShown) { this.thumbnailViewer.hide(); }else{ this.thumbnailViewer.show(); } this.thumbnailShown = !this.thumbnailShown; }The method decorator is used here so that we can also call it like this:
const PDFViewer = document.querySelector("pdf-viewer"); PDFViewer.toggleThumbnailViewer();
Add Zoom Controls
Next, add an Input for setting zoom (display percent).
JSX:
<div class="zoom toolbar-item">
<input type="number" id="percent-input"
value={this.percent}
title="Percent"
onChange={(e) => this.updateZoom(e)}
/><label htmlFor="percent-input">%</label>
</div>
CSS:
#percent-input {
width: 4em;
}
The update zoom function:
@State() percent: number = 100;
updateZoom(e:any){
this.percent = e.target.value;
const zoom = this.percent/100;
this.DWObject.Viewer.zoom = zoom;
}
Add a Quick Fit-to-Window Toggle
Next, let’s add a toggle button to set the zoom percentage to 100% or make it fit the window.
JSX:
<div class="quicksize toolbar-item">
{this.showFitWindow
? <img title="Fit window" class="Icon" src={fitWindow} onClick={()=>this.quicksize()}/>
: <img title="Original size" class="Icon" src={origSize} onClick={()=>this.quicksize()}/>
}
</div>
The quicksize function:
@State() showFitWindow: boolean = true;
quicksize(){
if (this.showFitWindow) {
this.DWObject.Viewer.fitWindow("width");
this.percent = this.DWObject.Viewer.zoom*100;
}else{
this.DWObject.Viewer.zoom = 1.0;
this.percent = 100;
}
this.showFitWindow = !this.showFitWindow;
}
Add Page Navigation Controls
Add controls to display/set the page number and display the total page number.
JSX:
<div class="page toolbar-item">
<input type="number" id="page-input"
value={this.selectedPageNumber}
title="Page number"
onChange={(e) => this.updateSelectedPageNumber((e as any).target.value)}
/>/{this.totalPageNumber}
</div>
CSS:
#page-input {
width: 3em;
}
The updateSelectedPageNumber and updateTotalPage functions:
@State() totalPageNumber: number = 0;
@State() selectedPageNumber: number = 0;
updateSelectedPageNumber(num:number){
if (num <= 0 || num > this.totalPageNumber) {
this.selectedPageNumber = this.DWObject.CurrentImageIndexInBuffer + 1;
return;
}
this.selectedPageNumber = num;
let index = this.selectedPageNumber - 1;
if (this.DWObject) {
this.DWObject.SelectImages([index]);
}
}
updateTotalPage(){
if (this.DWObject) {
this.totalPageNumber = this.DWObject.HowManyImagesInBuffer;
}
}
Then, call the above functions in the OnBufferChanged event.
let pThis = this;
pThis.DWObject.RegisterEvent('OnBufferChanged',function (bufferChangeInfo) {
if (bufferChangeInfo.action === "add" || bufferChangeInfo.action === "remove") {
pThis.updateTotalPage();
}
pThis.updateSelectedPageNumber(pThis.DWObject.CurrentImageIndexInBuffer + 1);
});
Add Scan, Load, Save, and Fullscreen Actions
There are some extra actions we can add. Here, we use a button to toggle an overlay which contains buttons for these actions.
JSX for the button (a container is appended to make it right-aligned):
<div class="toolbar">
<div class="toolbar-container toolbar-item"></div>
<div class="action toolbar-item">
<img class="Icon" src={settings} onClick={()=>this.toggleActionOverlay()}/>
</div>
</div>
CSS:
.toolbar-container {
flex: 1;
}
The overlay for actions is appended to the container of Web TWAIN.
<div id={this.containerID} class="dwt-container" ref={(el) => this.dwtContainer = el as HTMLDivElement}>
{this.renderStatus()}
{this.renderActionOverlay()}
</div>
The toggleActionOverlay and renderActionOverlay function:
toggleActionOverlay(){
this.showActionOverlay = !this.showActionOverlay;
}
renderActionOverlay(){
let className:string;
if (this.showActionOverlay) {
className = "overlay";
}else{
className = "overlay hidden";
}
return (
<div class={className}>
{this.fullscreen
? <img title="Exit fullscreen" class="Icon" src={exitFullscreen} onClick={()=>this.toggleFullscreen()}/>
: <img title="Enter fullscreen" class="Icon" src={fullscreen} onClick={()=>this.toggleFullscreen()}/>
}
<img title="Scan documents" class="Icon" src={scanner} onClick={()=>this.scan()}/>
<img title="Load local file" class="Icon" src={openFile} onClick={()=>this.loadFile()}/>
<img title="Save to PDF" class="Icon" src={download} onClick={()=>this.saveFile()}/>
</div>
);
}
CSS for the overlay:
.overlay {
position: absolute;
display: flex;
flex-direction: column;
right: 5px;
top: 5px;
width: 22px;
height: 125px;
z-index: 999;
padding: 5px;
border-radius: 5%;
border: 1px solid rgb(204, 204, 204);
align-items: flex-start;
justify-content: space-between;
background: white;
}
.overlay.hidden {
visibility: hidden;
}
Next, let’s talk about the JavaScript functions for actions.
-
toggleFullscreenwhich makes the PDF viewer fullscreen. Since iPhone’s Safari does not support the fullscren API, we set the container to a fixed fullscreen element for Safari.async toggleFullscreen(){ let isSafari = Dynamsoft.Lib.env.bSafari; if (isSafari) { if (this.parentContainer.classList.contains("fullscreen")) { this.parentContainer.classList.remove("fullscreen"); this.fullscreen = false; this.resizeViewer(); }else{ this.parentContainer.classList.add("fullscreen"); this.fullscreen = true; this.resizeViewer(); } }else{ if (document.fullscreenElement) { document.exitFullscreen(); }else{ let ele = this.parentContainer.parentNode["host"]; await ele.requestFullscreen(); } } } resizeViewer(){ this.DWObject.Viewer.singlePageMode = true; }CSS:
.container.fullscreen { position: fixed; left: 0; top: 0; width: 100%; height: 100%; z-index: 990; } -
loadFilewhich loads local PDF or image files.loadFile(){ const success = () => { this.updateTotalPage(); } this.DWObject.LoadImageEx("",Dynamsoft.DWT.EnumDWT_ImageType.IT_ALL,success); } -
scanwhich scans documents from scanners or cameras (for mobile devices).scan(){ let pThis = this; if (Dynamsoft.Lib.env.bMobile) { pThis.DWObject.Addon.Camera.scanDocument(); }else{ pThis.DWObject.SelectSource(function () { pThis.DWObject.OpenSource(); pThis.DWObject.AcquireImage(); }, function () { console.log("SelectSource failed!"); } ); } } -
saveFilewhich saves the document as a PDF file.saveFile(){ this.DWObject.SaveAllAsPDF("scanned.pdf"); }
The final result is like the following:

Common Issues and Edge Cases
- ShadowDOM blocks Dynamic Web TWAIN initialization: Because web components use ShadowDOM, the standard
Dynamsoft.DWT.CreateDWTObjectmethod cannot find the container element. UseCreateDWTObjectExinstead, which accepts a WebTwainId and lets you manually bind the viewer to a ShadowDOM-hosted element. - Cross-origin PDF loading fails: If the PDF URL is hosted on a different domain without proper CORS headers, the
fetchcall inloadPDF()will fail silently. Ensure the server serving the PDF setsAccess-Control-Allow-Originor host the file on the same domain. - Fullscreen API not supported on iOS Safari: The
requestFullscreen()API is not available in Safari on iPhone. The component includes a fallback that applies a fixed-position CSS class to simulate fullscreen, but the behavior differs slightly from native fullscreen on other browsers.
Source Code
Get the source code of the PDF viewer web component to have a try: