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() with ssr: 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

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:

  1. 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;
      }
    },[]);
    
  2. 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;
          }
        }
      });
    },[]);
    
  3. 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.

  1. Because Dynamic Web TWAIN uses navigator which 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>,
    });
    
  2. Update index.tsx to 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>
        </>
      )
    }
    
  3. 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>
      </>
    )
    
  4. 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 defined error at build time: Dynamic Web TWAIN accesses navigator and 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 with dynamic(() => import(...), { ssr: false }).
  • Scanner not detected or SelectSource returns 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 div must have an explicit width and height. If you use percentage-based sizing, make sure the parent elements also have defined dimensions.

Source Code

https://github.com/tony-xlh/NextJS-Document-Scanner