Render PDF annotations and forms with PDF.js AnnotationLayer
Table of contents
AnnotationLayer that renders annotations already embedded in a PDF — links, form fields, popups, text notes, and more. This tutorial covers enabling interactive forms, reading form values, and customizing annotation appearance.
- The
AnnotationLayerrenders annotations that already exist in the PDF — links, form widgets, highlights, and sticky notes. It’s separate from theAnnotationEditorLayer, which creates new annotations. - Set
annotationMode: AnnotationMode.ENABLE_FORMSonPDFViewerto make form fields interactive; enable XFA withenableXfa: true. - Read field values by iterating
page.getAnnotations()and filtering onsubtype === "Widget"—fieldName,fieldValue, andfieldType("Tx","Btn","Ch") are all there.
Prerequisites
This guide assumes you have a PDFViewer from pdfjs-dist/web/pdf_viewer.mjs and a loaded pdfDocument. If you haven’t wired those up yet, start with our blog on how to set up a custom PDF.js viewer in React. For creating new annotations rather than rendering existing ones, see the AnnotationEditorLayer guide.
You’ll also need:
pdfjs-dist4.x or later- A PDF with annotations or form fields to test against
What the AnnotationLayer renders
| Annotation type | Rendered as |
|---|---|
| Link | Clickable <a> elements |
| Text (popup note) | Note icon with popup on click |
| Highlight/underline/strikeout | Colored overlays |
| Free text | Positioned text block |
| Widget (form fields) | <input>, <select>, <textarea> |
| Stamp | Image |
| File attachment | Download icon |
Enabling the AnnotationLayer
The PDFViewer renders annotation layers by default. If building a custom page renderer, you enable it per page:
import { AnnotationLayer } from "pdfjs-dist";
async function renderAnnotationLayer(page, viewport, container) { const annotations = await page.getAnnotations();
const annotationLayerDiv = document.createElement("div"); annotationLayerDiv.className = "annotationLayer"; container.appendChild(annotationLayerDiv);
const annotationLayer = new AnnotationLayer({ div: annotationLayerDiv, accessibilityManager: null, annotationCanvasMap: null, page, viewport: viewport.clone({ dontFlip: true }), });
await annotationLayer.render({ annotations, div: annotationLayerDiv, page, viewport: viewport.clone({ dontFlip: true }), linkService, });}Rendering interactive forms
PDF forms with text inputs, checkboxes, radio buttons, and dropdowns become fully interactive:
const viewer = new pdfjs.PDFViewer({ container, eventBus, linkService, findController, // Enable interactive form rendering annotationMode: pdfjs.AnnotationMode.ENABLE_FORMS,});AnnotationMode options
PDFViewer accepts one of four annotation modes:
const AnnotationMode = { DISABLE: 0, // Don't render annotations at all. ENABLE: 1, // Render annotations (read-only). ENABLE_FORMS: 2, // Render annotations with interactive forms. ENABLE_STORAGE: 3, // Interactive forms with local storage persistence.};Reading form values
After the user fills in a form, you can extract values:
async function getFormData(pdfDocument) { const formData = {};
for (let i = 1; i <= pdfDocument.numPages; i++) { const page = await pdfDocument.getPage(i); const annotations = await page.getAnnotations();
for (const annot of annotations) { if (annot.subtype === "Widget") { formData[annot.fieldName] = { type: annot.fieldType, // "Tx" (text), "Btn" (button), "Ch" (choice). value: annot.fieldValue, options: annot.options, // For dropdowns/listboxes. }; } } }
return formData;}XFA forms
PDF.js also supports XML Forms Architecture (XFA) — complex dynamic forms used in government and enterprise PDFs:
const viewer = new pdfjs.PDFViewer({ container, eventBus, linkService, enableXfa: true, // Enable XFA form rendering.});XFA forms are rendered as HTML rather than canvas, allowing full interactivity.
Reading existing annotations
Access all annotations on a page for custom processing:
async function listAnnotations(pdfDocument) { for (let i = 1; i <= pdfDocument.numPages; i++) { const page = await pdfDocument.getPage(i); const annotations = await page.getAnnotations();
for (const annot of annotations) { console.log({ type: annot.subtype, // "Link", "Text", "Highlight", "Widget", etc. rect: annot.rect, // [x1, y1, x2, y2] in PDF coordinates. contents: annot.contents, // Popup text content. color: annot.color, // `Uint8ClampedArray` of [r, g, b] (0–255). url: annot.url, // For link annotations. fieldName: annot.fieldName, // For form widgets. fieldValue: annot.fieldValue, }); } }}Annotation appearance
Customize how annotations appear with CSS:
/* Style popup annotations */.annotationLayer .popup { background-color: #FFFFCC; border: 1px solid #999; border-radius: 4px; box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); max-width: 300px;}
/* Style form fields */.annotationLayer .textWidgetAnnotation input { border: 1px solid #4A90D9; border-radius: 2px; padding: 2px 4px;}
.annotationLayer .buttonWidgetAnnotation.checkBox input { accent-color: #4A90D9;}Key points
- The
AnnotationLayerrenders annotations that already exist in the PDF (not user-created ones). - Use
AnnotationMode.ENABLE_FORMSto make form fields interactive. page.getAnnotations()returns structured data for all annotations on a page.- XFA forms need
enableXfa: trueon the viewer. - The
AnnotationLayeris separate from theAnnotationEditorLayer(editing) and any custom overlay layer. - CSS classes on
.annotationLayerlet you customize the appearance of all native annotations.
How Nutrient Web SDK handles annotations and forms
Nutrient Web SDK renders all annotation types and form widgets interactively without an AnnotationMode flag, and it exposes form values directly on the instance without requiring per-page iteration to collect them. Learn more about forms for the full create-and-fill workflow.
const formFields = await instance.getFormFields();
await instance.setFormFieldValues({ "First Name": "Jane", "Last Name": "Doe", "Email": "jane@example.com",});Beyond rendering, the SDK supports form validation, calculation fields, JavaScript actions, and digital signatures (PKCS#7) — features that PDF.js leaves to your application. For roundtripping form values back into the PDF binary, refer to our post on how to export PDFs with embedded annotations.
Learn more about Nutrient Web SDK | Migration guide | Contact Sales
FAQ
AnnotationLayer and AnnotationEditorLayer?AnnotationLayer renders annotations that already exist in the PDF (read or fill, but not create). AnnotationEditorLayer is the editing UI for new annotations the user draws — free text, ink, stamp, highlight. They can coexist: Existing PDF annotations show through the AnnotationLayer, and new edits go through the AnnotationEditorLayer. See the AnnotationEditorLayer guide for the creation side.
annot.fieldType actually contain?The PDF specification defines these field type codes: "Tx" (text field), "Btn" (button — push button, checkbox, or radio button), "Ch" (choice — list box or dropdown), and "Sig" (signature). To distinguish push button from checkbox from radio, check the annot.checkBox, annot.radioButton, and annot.pushButton flags.
ENABLE_STORAGE for?AnnotationMode.ENABLE_STORAGE keeps form values in PDF.js’s internal AnnotationStorage, which survives across rerenders within the same session and feeds into pdfDocument.saveDocument(). Use it when you want users to fill a form. Then download the filled PDF without writing your own value-tracking layer.
They matter less than they used to. XFA was an Adobe-only format for dynamic forms, deprecated in PDF 2.0. You’ll still encounter XFA in legacy government and enterprise PDFs, so PDF.js’s enableXfa: true support is useful when migrating from older systems — but newer forms use AcroForm widgets (the Widget subtype) instead.
Either you haven’t set annotationMode: ENABLE_STORAGE (values stay only in the DOM input) or the form fields don’t have stable fieldName attributes. PDF.js keys storage by field name, so duplicate or empty names cause values to overwrite or vanish. Inspect annot.fieldName from getAnnotations() to confirm.
You have two options. First, pdfDocument.saveDocument() writes the current AnnotationStorage values into a new PDF byte stream. This works for forms PDF.js can fill in place. Second, for forms you can’t edit through PDF.js, or for richer roundtrips, read values, and then write a fresh PDF with a library like annotpdf or pdf-lib.