Garry Klooesterman
Senior Technical Content Creator
Updated February 26, 2026
5 min
Garry Klooesterman
Senior Technical Content Creator

Summary: Building forms in the browser can be as simple as adding an <input> tag and some CSS. But PDF forms don’t play by the same rules. If you’ve ever tried to use document.getElementById on a PDF field, you know the pain. This blog discusses why PDF forms are unique and how to master them using JavaScript and Apryse WebViewer.

If you’re coming from a web dev background, PDF forms may feel like a different dimension, as creating PDF forms in JavaScript works completely differently than building HTML forms. Here’s why:
PDFs have no DOM: Unlike an HTML page, a PDF is a group objects. There are no tags, no divs, and no inherited styles. Fields are stored as dictionaries in the file’s internal tree.
AcroForms vs. XFA: Apryse WebViewer supports AcroForms, the universal standard for interactive, fillable PDF forms. XFA (XML Forms Architecture) is an older, proprietary Adobe format that is difficult to render in modern browsers and was deprecated in the PDF 2.0 ISO standard.
The y-Axis Flip: In HTML, (0,0) is the top-left corner of the document, but in a PDF, the origin (0,0) is at the bottom-left.
Library Limits: Most JS libraries are headless (Node-only) or lack a visual UI. Designing a form blindly through coordinates can be difficult to ensure everything is in the right place.
Apryse WebViewer is a powerful and robust JavaScript Document SDK. It’s a fully client-side SDK with many features such as high-fidelity document viewing, annotation, conversion, and editing across 30+ formats.
Here’s why developers choose WebViewer for creating fillable PDF forms:
Before we start coding, let's get the environment ready. You’ll need to make sure you’ve done these steps first, then we’ll get started.
First, we need to install WebViewer into your project using the following command on the command line from your project directory:
npm i @pdftron/webviewerPaste the files you copied to the new lib/webviewer public location.

Figure 1: Folder structure for our project.
Now let’s add some code to the index.html you created. Copy the following HTML code and paste it into the file.
Note:
1. The next thing we need to add to the index.html file is the following JavaScript code.
Note: Place it after the div and before the close body tag.
2. Confirm that the paths to the static files for core, ui, and webviewer.min.js are correct.
3. Add your license key in place of 'YOUR_LICENSE_KEY'.
Note: If you've created your license key and you're logged in, your key is already in the code below.
4. Choose an initial document to open when WebViewer starts using the initialDoc parameter and replace 'YOUR_DOC_HERE' with the path and filename.
5. Specify the id of the element where you want WebViewer to be mounted. In this case, that's viewer.
View a PDF in WebViewer UI
1. Save all the files and you’re now ready to serve the webpage so you can see the WebViewer UI and the PDF you included to open in WebViewer.
2. Run the following command on the command line from your project directory to run http-server, which is a simple web server.
npx http-server -a localhost3. Click the localhost link created in your terminal to view the WebViewer UI and PDF file locally.
Ok great, so we’ve set up our project and WebViewer and we’ve opened a PDF. Let’s move on to the best part and make this a fillable PDF form.
In this example, we’re going to programmatically create a text field. In a PDF, a form field consists of two parts: the Field (the data/logic) and the Widget (the visual part the user sees).
We’ll add a text field and text widget annotation using this code:
And there we have it. We’ve programmatically added a text field and text widget annotation to a PDF.
Here’s an example of what this would look like on a PDF.

Figure 2: Our PDF with the new text field added.
There’s a lot happening in that code sample so let’s take a look at what’s going on:
The Apryse WebViewer SDK fully supports reading, writing, and editing PDF forms. So, after a form field is filled with data, the SDK can read and extract that data to JSON format to be used later.
If you’re looking for a way to set up fields with less coding, you can do it directly in the WebViewer UI. Check out the demo!
What’s the difference between a Field and a Widget?
Doing a comparison, a Field is the variable name in your code (the data), and the Widget is the CSS/HTML input on the screen (the UI).
Can I make fields required?
Yes. You can use WidgetFlags when creating your field.
Does this support digital signatures?
Yes. You can add e-signatures and advanced digital signatures backed by certificate authorities.
Can I customize the look of the fields (colors, fonts)?
Yes. You can set various elements including the stroke color, fill color, and font size directly on the WidgetAnnotation.
Creating PDF forms in JavaScript can be easy if you’re using a robust SDK like Apryse WebViewer. You can treat PDF objects with the same ease as DOM elements while letting the engine handle the complexities of the PDF specification.
Want to try it out for yourself? Check out our demo or start a free trial.
You can also contact our sales team for any questions.
<html>
<head>
<title>Basic WebViewer</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no" />
</head>
<!-- Import WebViewer as a script tag -->
<script src='/lib/webviewer/webviewer.min.js'></script>
<body>
<div id='viewer' style='width: 1024px; height: 600px; margin: 0 auto;'></div>
</body>
</html> <script>
WebViewer({
path: '/lib/webviewer', // Path to the 'lib' folder on your server
licenseKey: 'YOUR_LICENSE_KEY', //Sign up to get a license key at https://dev.apryse.com
initialDoc: 'YOUR_DOC_HERE'
// initialDoc: '/path/to/my/file.pdf', // You can also use documents on your server
}, document.getElementById('viewer'))
.then(instance => {
const { documentViewer, annotationManager } = instance.Core;
// Call methods from instance, documentViewer, and annotationManager as needed
// You can also access major namespaces from the instance as follows:
// const Tools = instance.Core.Tools;
// const Annotations = instance.Core.Annotations;
// call methods relating to the loaded document
documentViewer.addEventListener('documentLoaded', () => {
});
})
</script>WebViewer(...)
.then(instance => {
const { annotationManager, Annotations, documentViewer } = instance.Core;
const { WidgetFlags } = Annotations;
documentViewer.addEventListener('documentLoaded', () => {
// Sets flags for the text widget.
const flags = new WidgetFlags();
flags.set(WidgetFlags.REQUIRED, true);
flags.set(WidgetFlags.MULTILINE, true);
// Creates a text form field.
const field = new Annotations.Forms.Field('TextFormField 1', {
type: 'Tx',
defaultValue: 'Default Value',
flags,
});
// Creates a text widget annotation.
const widgetAnnot = new Annotations.TextWidgetAnnotation(field);
widgetAnnot.PageNumber = 1;
widgetAnnot.X = 100;
widgetAnnot.Y = 100;
widgetAnnot.Width = 200;
widgetAnnot.Height = 50;
// Add form field to field manager and widget annotation to annotation manager.
annotationManager.getFieldManager().addField(field);
annotationManager.addAnnotation(widgetAnnot);
annotationManager.drawAnnotationsFromList([widgetAnnot]);
});
});