Build a Document Scanner Web Component with LitElement and Dynamic Web TWAIN
Lit is a simple library for building fast, lightweight web components. At Lit’s core is a boilerplate-killing component base class that provides reactive state, scoped styles, and a declarative template system that’s tiny, fast and expressive.
In this article, we are going to build a web component with LitElement to scan documents in an HTML website based on Dynamic Web TWAIN.
What you’ll build: A reusable <document-scanner> web component using LitElement and Dynamic Web TWAIN that can scan, view, and save documents as PDF — embeddable in any HTML page.
Key Takeaways
- LitElement lets you build a framework-agnostic document scanner web component that works in any HTML page with a single custom element tag.
- Dynamic Web TWAIN handles TWAIN scanner communication, image buffering, and PDF export through its JavaScript SDK.
- The
firstUpdatedLit lifecycle hook is the correct place to initialize the Dynamic Web TWAIN object and bind it to the shadow DOM container. - The finished component exposes scan, save, and image-count features and dispatches a custom
initializedevent for parent-level control.
Common Developer Questions
How do I build a document scanner web component with LitElement?
Create the custom element with Lit, render a scanner container in the template, and initialize Dynamic Web TWAIN in firstUpdated() so the shadow DOM target already exists. The component then exposes scan, save, and image-count behavior without depending on any framework outside the custom element.
How do I integrate Dynamic Web TWAIN with Lit and webpack?
Install dwt from npm, copy its runtime resources into the public folder during the webpack build, and point ResourcesPath to that served location. Without the copied resources, the viewer renders but the scanner runtime files never load correctly.
How do I save scanned documents as PDF from a browser using a web component?
Once the WebTwain object is ready, call the SDK’s PDF save API from the component and use the current image buffer as the export source. The same component can also track image count changes through buffer events so the UI stays in sync after scanning or deleting pages.
Prerequisites
Before you start, make sure you have:
- Node.js (v14 or later) and npm installed
- A physical scanner connected to your machine (or use the virtual scanner for testing)
- A Dynamic Web TWAIN license key. Get a 30-day free trial license to follow along.
Step 1: Set Up a New Webpack Project
Although Lit can be used without a build system, in this article, we are going to use webpack with the following template (clone with git):
git clone https://github.com/wbkd/webpack-starter
Step 2: Install Lit and Dynamic Web TWAIN
-
Install Lit.
npm install lit -
Install Dynamic Web TWAIN.
npm install dwtIn addition, we need to copy the resources of Dynamic Web TWAIN to the public folder.
-
Install
ncp.npm install --save-dev ncp -
Modify
package.jsonto copy the resources for the build and start commands."scripts": { "lint": "npm run lint:styles; npm run lint:scripts", "lint:styles": "stylelint src", "lint:scripts": "eslint src", - "build": "cross-env NODE_ENV=production webpack --config webpack/webpack.config.prod.js", - "start": "webpack serve --config webpack/webpack.config.dev.js" + "build": "ncp node_modules/dwt/dist public/dwt-resources && cross-env NODE_ENV=production webpack --config webpack/webpack.config.prod.js", + "start": "ncp node_modules/dwt/dist public/dwt-resources && webpack serve --config webpack/webpack.config.dev.js" }, -
Modify
webpack.common.jsto copy the files in thepublicfolder to the output folder instead of thepublicfolder inside the output folder.new CopyWebpackPlugin({ - patterns: [{ from: Path.resolve(__dirname, '../public'), to: 'public' }], + patterns: [{ from: Path.resolve(__dirname, '../public'), to: '' }], }),
-
Step 3: Build the Document Scanner Web Component
-
Create a new
documentscanner.jsfile undersrc\scriptswith the following template.import {LitElement, html, css} from 'lit'; export class DocumentScanner extends LitElement { static properties = { }; DWObject; static styles = css` :host { display: block; } `; constructor() { super(); } render() { return html``; } } customElements.define('document-scanner', DocumentScanner); -
Add a
divelement as the container for the controls of Dynamic Web TWAIN (mainly to view documents).render() { return html`<div id="dwtcontrolContainer"></div>`; } -
Configure Dynamic Web TWAIN in the
constructor. You need a license to use Dynamic Web TWAIN. You can apply for a license here.import Dynamsoft from 'dwt'; export class DocumentScanner extends LitElement { constructor() { super(); Dynamsoft.DWT.AutoLoad = false; Dynamsoft.DWT.ResourcesPath = "/dwt/dist"; Dynamsoft.DWT.ProductKey = "LICENSE-KEY"; } } -
Initialize Dynamic Web TWAIN in the
firstUpdatedlifecycle after the dom is first updated and bind it to the container in the previous step. In addition, dispatch a custom event with the object of Dynamic Web TWAIN so that we can control it in a parent node.DWObject; firstUpdated() { let pThis = this; let dwtContainer = this.renderRoot.getElementById("dwtcontrolContainer"); Dynamsoft.DWT.CreateDWTObjectEx( { WebTwainId: 'dwtcontrol' }, function(obj) { pThis.DWObject = obj; pThis.DWObject.Viewer.bind(dwtContainer); pThis.DWObject.Viewer.show(); pThis.DWObject.Viewer.width = "100%"; pThis.DWObject.Viewer.height = "100%"; const event = new CustomEvent('initialized', { detail: { DWObject: pThis.DWObject } }); pThis.dispatchEvent(event); }, function(err) { console.log(err); } ); } -
Add one button to scan documents and one button to save the document images as a PDF file.
render() { return html` <div class="buttons"> <button @click=${this.scan}>Scan</button> <button @click=${this.save}>Save</button> </div> <div id="dwtcontrolContainer"></div>`; } scan(){ let pThis = this; if (pThis.DWObject) { pThis.DWObject.SelectSource(function () { pThis.DWObject.OpenSource(); pThis.DWObject.AcquireImage(); }, function () { console.log("SelectSource failed!"); } ); } } save(){ if (this.DWObject) { this.DWObject.SaveAllAsPDF("Scanned.pdf"); } } -
Add a reactive property named
totalto reflect how many documents are scanned. We can update its value in theOnBufferChangedevent of Web TWAIN.export class DocumentScanner extends LitElement { static properties = { total: {}, }; constructor() { super(); this.total = 0; //... } render() { return html` <div class="buttons"> <button @click=${this.scan}>Scan</button> <button @click=${this.save}>Save</button> </div> <div id="dwtcontrolContainer"></div> <div class="status">Total: ${this.total}</div>`; } firstUpdated() { //... Dynamsoft.DWT.CreateDWTObjectEx( { WebTwainId: 'dwtcontrol' }, function(obj) { //... pThis.DWObject.RegisterEvent('OnBufferChanged',function () { pThis.total = pThis.DWObject.HowManyImagesInBuffer; }); //... }, function(err) { console.log(err); } ); } } -
Set the styles for the component.
static styles = css` :host { display: block; } .buttons { height: 25px; } #dwtcontrolContainer { width: 100%; height: calc(100% - 50px); } .status { height: 25px; } `;
Step 4: Embed the Scanner Component in Your Page
-
Import the component in the
index.jsfile.import { DocumentScanner } from './documentscanner'; // eslint-disable-line -
Add the component in the
index.html.<document-scanner style="width:320px;height:480px;" ></document-scanner>
All right, we can now scan documents from the browser.

Common Issues and Edge Cases
- “Dynamic Web TWAIN resources not found” error: This usually means the
ncpcopy step did not run before the dev server started. Make sure thestartandbuildscripts inpackage.jsoninclude thencp node_modules/dwt/dist public/dwt-resourcesprefix as shown in Step 2. - Scanner not detected in the browser: Dynamic Web TWAIN requires the Dynamsoft Service to be installed on the client machine for TWAIN communication. If
SelectSourcereturns no scanners, verify the service is running and accessible. - Shadow DOM blocks the viewer container: Because LitElement renders into a shadow root, you must use
this.renderRoot.getElementById()instead ofdocument.getElementById()to locate the container — standard DOM queries will not find elements inside the shadow tree.