How to Build a Web Document Scanner in Next.js with Dynamic Web TWAIN
Next.js is the recommended framework to create React applications with extra features like server-side rendering and an intuitive router.
In this article, we are going to create a document scanning app with Next.js and Dynamic Web TWAIN.
What you’ll build: A browser-based document scanning app using Next.js and the Dynamic Web TWAIN SDK that can acquire pages from a physical scanner, preview them in a multi-page viewer, and export to PDF.
Key Takeaways
- Dynamic Web TWAIN can be wrapped as a reusable React component and loaded client-side in Next.js using
dynamic()withssr: false. - The SDK connects to TWAIN/WIA/SANE-compatible physical scanners directly from the browser, with no server-side scanning logic required.
- Scanned documents can be previewed in a built-in multi-page viewer and saved as PDF with a single API call (
SaveAllAsPDF). - This approach works on Windows, macOS, and Linux desktops with a locally installed scanner driver.
Common Developer Questions
- How do I integrate a document scanner into a Next.js React app?
- Why does Dynamic Web TWAIN throw errors with Next.js server-side rendering?
- How do I save scanned documents as a PDF file in the browser using JavaScript?
Prerequisites
- Node.js 16 or later
- A TWAIN-compatible document scanner connected to your machine
- Get a 30-day free trial license for Dynamic Web TWAIN
Step 1: Create a New Next.js Project
Create a new Next.js project named document-scanner:
npx create-next-app@latest document-scanner
Then, we can run the following to test it:
cd document-scanner
npm run dev
Step 2: Install Dynamic Web TWAIN
Install Dynamic Web TWAIN from npm:
npm install dwt@19.3.0
Step 3: Create a Reusable Document Scanner Component
Dynamic Web TWAIN provides a document viewer control and a bunch of APIs to scan and manage documents. We are going to wrap the viewer as a React component and expose the object of Dynamic Web TWAIN to call different APIs.
Here is the basic content of the component:
interface props {
onWebTWAINReady?: (dwt:WebTwain) => void;
}
const DWT: React.FC<props> = (props: props) => {
const containerID = "dwtcontrolContainer";
const container = useRef<HTMLDivElement>(null);
useEffect(()=>{
Dynamsoft.DWT.RegisterEvent('OnWebTwainReady', () => {
const DWObject = Dynamsoft.DWT.GetWebTwain(containerID);
if (props.onWebTWAINReady) {
props.onWebTWAINReady(DWObject); // expose the object of Dynamic Web TWAIN
}
});
Dynamsoft.DWT.ResourcesPath = "https://unpkg.com/dwt@19.3.0/dist"; //use the resources from a CDN
Dynamsoft.DWT.Containers = [{
WebTwainId: 'dwtObject',
ContainerId: containerID
}];
Dynamsoft.DWT.Load();
},[]);
return (
<div ref={container} id={containerID}></div>
);
}
export default DWT;
Configure Component Props for License, Layout, and View Mode
There are some additional props we can add to the component:
-
A license to activate Dynamic Web TWAIN. We can apply for a trial license here.
interface props { license?:string; } useEffect(()=>{ if (props.license) { Dynamsoft.DWT.ProductKey = props.license; } },[]); -
Width and height for the viewer.
interface props { width?: string; height?: string; } useEffect(()=>{ Dynamsoft.DWT.RegisterEvent('OnWebTwainReady', () => { const DWObject = Dynamsoft.DWT.GetWebTwain(containerID); DWObject.Viewer.width = "100%"; DWObject.Viewer.height = "100%"; if (props.width) { if (container.current) { container.current.style.width = props.width; } } if (props.height) { if (container.current) { container.current.style.height = props.height; } } }); },[]); -
View mode for the viewer:
interface props { viewMode?: {cols:number,rows:number}; } useEffect(()=>{ Dynamsoft.DWT.RegisterEvent('OnWebTwainReady', () => { const DWObject = Dynamsoft.DWT.GetWebTwain(containerID); if (props.viewMode) { DWObject.Viewer.setViewMode(props.viewMode.cols,props.viewMode.rows); } }); },[]);
Step 4: Integrate the Component into Your Next.js Page
Next, we are going to use the component in the app.
-
Because Dynamic Web TWAIN uses
navigatorwhich is only available in the browser, we need to disable server-side rendering for the component. We can import the component with the following code to do this:const DWT = dynamic(() => import("../components/DWT"), { ssr: false, loading: () => <p>Initializing Document Scanner</p>, }); -
Update
index.tsxto use the component.import { WebTwain } from 'dwt/dist/types/WebTwain'; import dynamic from 'next/dynamic'; import Head from 'next/head' import styles from '../styles/Home.module.css' import React from 'react'; const DWT = dynamic(() => import("../components/DWT"), { ssr: false, loading: () => <p>Initializing Document Scanner</p>, }); export default function Home() { const DWObject = React.useRef<WebTwain>(); const onWebTWAINReady = (dwtObject:WebTwain) => { DWObject.current = dwtObject; } return ( <> <Head> <title>Next.js Document Scanner</title> <meta name="description" content="Generated by create next app" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <link rel="icon" href="/favicon.ico" /> </Head> <main> <div> <h2>Document Scanner</h2> <div className={styles.container}> <DWT width='100%' height='100%' viewMode={{cols:2,rows:2}} onWebTWAINReady={onWebTWAINReady} ></DWT> </div> </div> </main> </> ) } -
Add a scan button to scan documents.
const scan = () => { if (DWObject.current) { DWObject.current.SelectSource(function () { DWObject.current!.OpenSource(); DWObject.current!.AcquireImage(); }, function () { console.log("SelectSource failed!"); } ); } } return ( <> <main> <div> <h2>Document Scanner</h2> <button onClick={scan}>Scan</button> </div> </main> </> ) -
Add a save button to save the scanned documents into a PDF file.
const save = () => { if (DWObject.current) { DWObject.current.SaveAllAsPDF("Scanned"); } } return ( <> <main> <div> <h2>Document Scanner</h2> <button onClick={save}>Save</button> </div> </main> </> )
All right, we’ve now finished the document scanner. You can use the online demo to have a try.
Common Issues & Edge Cases
navigator is not definederror at build time: Dynamic Web TWAIN accessesnavigatorand other browser-only APIs. If the component is imported normally, Next.js will try to render it on the server and crash. Always import it withdynamic(() => import(...), { ssr: false }).- Scanner not detected or
SelectSourcereturns empty: Ensure a TWAIN/WIA/SANE driver is installed for your scanner and that the Dynamsoft Service is running locally. The SDK communicates with scanners through this background service. - Viewer does not appear or has zero dimensions: The container
divmust have an explicit width and height. If you use percentage-based sizing, make sure the parent elements also have defined dimensions.