COMING SOON: Spring 2025 Release

Creating an Autocue App Using WebViewer, React, and JavaScript

By Roger Dunham | 2025 Mar 28

Sanity Image
Read time

6 min

Summary: Discover how to create a simple app using WebViewer, React, and JavaScript to automatically scroll PDFs while narrating videos.

I write a lot of blog articles for Apryse, and from time to time I also create tutorial videos.

When I’m narrating a video, I usually work from a script. But there’s a problem, when I get to the end of the page, I either need to stop, or else keep recording and edit the audio later to remove the delay that occurs when I scroll to the next page.

To solve this problem, I put together a simple app that leverages Apryse WebViewer, React and JavaScript, and allows a PDF to be loaded into then automatically scrolled within the browser. It certainly made my life easier.

In this article we will look at how to achieve this. You can then extend the code to control WebViewer in other ways to make your life easier too.

WebViewer is a pure JavaScript Document SDK, which has a customizable open-source UI. It is fully featured, secure, and meets Accessibility guidelines.

Learn more about WebViewer and how it can help you with your business needs.

Prerequisites

Copied to clipboard

The sample code used in this article was scaffolded using Vite and written using Windows and Node 22. It should work with Linux and macOS too, as well as with several earlier versions of Node, but I haven’t verified that. If there’s a problem, then please let me know.

Step 1 – Create a new Vite app

In the folder where you store your source code (in my case “C:\demo”) enter:

npm create vite@latest

This will then lead you through a series of prompts. I chose to call my project “autocue” and to use TypeScript.

Blog image

Figure 1 - Scaffolding the project using "create-vite".

Step 2 - Install dependencies

Once the scaffolding completes navigate to the newly created folder and install dependencies.

cd autocue
npm install
Blog image

Figure 2- Installing the dependencies.

Step 3 - Remove unnecessary code within a Code Editor

Next open the project in a code editor (I use VSCode, but you can use whatever you prefer). You will see that Vite has created a complex folder structure, which is great since you now have an app that can be run – which is a great way to verify that your system is configured correctly.

However, we don’t need most of what Vite has created so let’s remove everything that’s in “App.tsx”. We will rebuild that file in a minute with what we do need.

Step 4 - Install WebViewer

Next, we need to provide access to Apryse WebViewer which we can do using npm (but use a different package manager if you prefer).

npm install @pdftron/webviewer

This will add the WebViewer package to the node_modules.

Blog image

Figure 3 - The WebViewer files within node_modules.

Since we need our React code to have access to WebViewer on the client-side, we need to make sure that the files are also in the project’s “public” folder. Let’s create a new subfolder called “lib/webviewer” then copy the “core” and “ui” folders from node_modules and paste them into our new “public/lib/webviewer” folder.

While we are doing this manually, we could automate this using a tool called from a “postinstall” script. In fact, many of the WebViewer GitHub samples contain just such a tool so feel free to copy that into your project. I won’t add it here though to keep the code as simple to understand as possible.

Blog image

Figure 4 - The core and ui files that need to be available within the public folder so that they can be accessed by React.

Step 5 – Create a basic WebViewer project

Getting started with WebViewer is easy – you just need to create an HTML placeholder for the WebViewer when it is instantiated, and a useEffect to actually construct WebViewer.

Add the following code into “App.tsx” (which we previously removed the default code from):

import WebViewer, { WebViewerInstance } from '@pdftron/webviewer';
import { useEffect, useRef, useState } from 'react';
import './App.css'
function App() {
const viewer = useRef<HTMLDivElement>(null);
const [wvInstance, setWvInstance] = useState<WebViewerInstance>();
const licKey = 'licenseKey';
useEffect(() => {
WebViewer(
{
path: '/lib/webviewer',
licenseKey: licKey,
enableFilePicker: true,
},
viewer.current as HTMLDivElement
).then(async (instance) => {
const { documentViewer } = instance.Core;
documentViewer.addEventListener('documentLoaded', () => {
setWvInstance(instance);
})
})
  
}, []);
return (
<div className="App">
<div className="webviewer" ref={viewer}></div>
</div>
);
}
export default App

We’re doing a little more than we need to in order to just display WebViewer in the browser – we are also storing the “instance” object that is returned when the WebViewer instantiation promise resolves into the state variable. We will use that variable, wvInstance, in a few minutes.

Gotcha! If Webviewer is instantiated as a Web Component, which is the default behavior in v11, then do not store the instance variable until after a document has been loaded.

You’re almost ready to run the code – there’s just a couple of things to do first:

Update the CSS

Create-vite creates a default value of display:flex within the file index.css. That causes WebViewer to appear as a very narrow strip, so for now, let’s comment that out.

body {
margin: 0;
/* display: flex; */
place-items: center;
min-width: 320px;
min-height: 100vh;
}

Get a license key

Finally, since it’s free and easy to get a free trial license key, get one and update the value in the code that we have just created in App.tsx.

Step 5a – Run the sample

You could skip this step, but it’s always useful to make sure that code works when it is simple, rather than waiting to run it when it has become complex.

npm run dev

In a few moments you should see WebViewer appear in a browser – probably on port 5173.

If the website doesn’t appear automatically in a browser, then you can copy the link and paste it into the browser's address bar.

Once it is running, you can then use the filePicker to choose a PDF (or many other document types) and it will display within WebViewer.

Blog image

Figure 5 - WebViewer showing an example contract PDF, running within Chrome.

It is easy to customize the UI and add or remove buttons and menu items, but for now I won't make any changes. That also means that if I want to I can use the app to edit the PDF, or to insert annotations or any of the other huge range of functionality that it supports.

While WebViewer is impressive as it is, I promised you scrolling, so we need to add that.

Step 6 – Add buttons to allow auto-scrolling

In a few moments we will add code to control the place where the PDF scrolls to using the scrollTo function of the scrollViewElement that belongs to the document viewer object.

First though let’s add buttons to our app that allow us to interact with WebViewer.

It’s easy to add new buttons within the WebViewer UI, but for this article let’s add them to the HTML itself. We can do that by updating the “return” function to have four new buttons.

return (
<div className="App">
<div className='button-div'>
<div className='button-cue' onClick={() => initializeCue(delay)}><button>Start</button></div>
<div className='button-cue'onClick={stopCue}><button>Stop</button></div>
<div className='button-cue'onClick={slower}><button>Slower</button></div>
<div className='button-cue'onClick={faster}><button>Faster</button></div>
</div>
<div className="webviewer" ref={viewer}></div>
</div>
);

We also need to declare some state variables to store the “interval” object (think of it as a timer that fires at regular intervals, rather than just once), the topPos location where we are scrolling to, and the delay that controls how often the scrolling code will occur.

Since we will let the user change the speed at which the PDF scrolls, we need to specify limits of the time between scroll events (I’ve chosen from 9 to 500ms, but you might want to use something different).

const [myInterval, setMyInterval] = useState(null)
const [topPos, setTopPos] = useState(0)
const [delay, setDelay] = useState(50)
const MIN_DELAY=9;
const MAX_DELAY=500;

We then have a set of three functions for altering the time between the document scrolling, making it faster, slower, or stopping it entirely.

const faster = () => {
const newDelay = delay - 10
if (newDelay > MIN_DELAY) {
setDelay(newDelay)
initializeCue(newDelay)
}
}
const slower = () => {
const newDelay = delay + 10
if (newDelay < MAX_DELAY) {
setDelay(newDelay)
initializeCue(newDelay)
}
}
const stopCue = () => {
initializeCue(0)
}

Finally, we get to the function that is doing the actual scrolling.

Within the loop of the interval timer, the value for the top position is incremented and the document is scrolled to that new position. When we get to the bottom of the document the interval timer is stopped since there is no more work to be done.

Since we want to allow the user to change the scroll speed, and we can’t easily change the delay from within a running interval timer, every time that delay is changed, initializeCue is called again clearing the old interval and creating a new one with the new delay time.

const initializeCue = (delay: number) => {
if (myInterval) {
clearInterval(myInterval);
setMyInterval(null)
}
if (delay > 0) {
let pos = topPos
//We could refactor the following code so that the values are stored as state varibales.
//for now though we will keep them here to demonstrate how they can be used.
const documentViewer = wvInstance!.Core.documentViewer;
const scrollViewElement = documentViewer.getScrollViewElement();
const fin = documentViewer.getViewerElement().scrollHeight;
var interval = setInterval(() => {
scrollViewElement.scrollTo({
top: pos,
left: 0
});
//This is a kludgy solution - since scrolling will occur, while doing nothing until the
//scroll point reaches the bottom of the viewer element. In fact, once everything is visible
//scrolling could stop.
if (pos < fin) {
//scrolling by 1 unit at a time means that the scroll action is smooth.
pos = pos += 1;
setTopPos(pos)
}
else {
clearInterval(interval);
}
}, delay);
setMyInterval(interval);
}
}

There are a few tweaks needed within App.css to make the WebViewer occupy the full height of the page, and to make the buttons look better.

.webviewer {
height:100vh;
}
.button-div{
display: inline;
}
button{
display: inline;
background-color: green!important;
color:white!important;
border-radius: 10px;
padding:5px;
margin: 20px;
}
.button-cue{
display: inline-block;
}

Step 7 - Run the code again

That’s everything in place.

Now when you run the project you can start the page scrolling, stop it, speed it up or slow it down by using the buttons.

Blog image

Figure 6 – The working autoscroll app.

Getting the code

Copied to clipboard

If you don’t want to follow all of the steps yourself, that’s OK – we have the entire code for this article available on GitHub.

Where next?

Copied to clipboard

Interacting with WebViewer using JavaScript is just one of the many things that it offers. It’s a massively powerful library offering the ability to securely redact documents, or add  annotations to PDFs, or to work directly with DOCX files without needing additional dependencies or licenses – all while keeping your data securely on your own machine.

There are dozens of ways in which WebViewer and the Apryse SDK can help your document processing needs, so try things out and see how much time you can save.

If you have any questions, then please reach out to us on our Discord channel.

Sanity Image

Roger Dunham

Share this post

email
linkedIn
twitter