EmbedPDF

Forms

A PDF form is a set of fields: text boxes, checkboxes, radio buttons, dropdowns and lists. The engine reads every field with its value, fills fields in, and redraws them so the new value shows in every PDF app.

const { fields } = await doc.forms.list();
 
const name = fields.find((field) => field.name === 'billing.name')!;
await doc.forms.setValue(name.ref, { value: 'Ada Lovelace' });

Read the form#

list() returns every field in the document:

const { fields } = await doc.forms.list();
 
for (const field of fields) {
  console.log(field.name, field.family);
}

Every field has these:

FieldWhat it is
nameThe full name, with the groups it’s in: billing.name.
family'text', 'checkbox', 'radio', 'combobox', 'listbox', 'pushbutton', 'signature' or 'unknown'.
refIts address. Keep it: it’s how you fill, reset or change the field.
readOnlytrue when PDF apps don’t let the user change the value.
requiredtrue when PDF apps ask for a value before the form is submitted.
noExporttrue when the field is left out when the form is submitted.
alternateNameThe tooltip PDF apps show, also read by screen readers, or null.
groupIdThe group the field belongs to: who fills it in, such as 'buyer', or null. See Signers and groups.
widgetsWhere the field shows on the pages (see Widgets).

A document without a form returns an empty fields. The list’s formKind says what the document has: 'none', 'acroform' (a regular PDF form) or 'xfa' (see Good to know).

The list also has needsAppearances, true when the form asks PDF apps to draw its fields themselves (repair() can clear it), and calculationOrder (see Calculations).

Values#

Each family has its own value fields. Check family first, and TypeScript knows which ones the field has:

if (field.family === 'text') console.log(field.value); // string
if (field.family === 'checkbox') console.log(field.checked); // boolean
if (field.family === 'listbox') console.log(field.selectedValues); // string[]
FamilyIts valueAlso
textvalue, a stringdefaultValue, maxLength (null: no limit), multiline, password, comb
checkboxcheckedexportValue: what the form data holds while it’s checked
radiovalue: the export value of the chosen button, or 'Off'noToggleToOff, radiosInUnison
comboboxvalueoptions, edit (free text allowed), defaultValue
listboxselectedValues, in option orderoptions, multiSelect, defaultValue (option values), topIndex (the option in its top row)
pushbuttonnone: a button only runs an actionEach widget’s caption, on its row in widgets
signaturenone here: see Digital signatures

Each option of a dropdown (combobox) or list is { label, value, selected }. label is what the user sees; value is what the form stores, and what you pass when you fill the field in.

Who created and filled it in#

A field records who created it and who last filled it in, from the identity the document was opened with:

field.createdBy; // 'user-17': who created the field, or null
field.filledBy; // 'user-42': whose fill last changed the value
field.filledByName; // 'Ann de Vries'
field.filledAt; // '2026-10-08T10:42:00Z'
FieldWhat it is
createdBy, createdAtWho created the field, and when. null for a field another app made.
filledBy, filledByName, filledAtThe user whose fill last changed the value, their name, and when. A calculated value counts as filled in by whoever’s fill calculated it. null after a reset, or an anonymous fill.
importedByWho imported the field with its attribution kept.

The engine stamps these; a write can’t set them. A fill that leaves the value as it was changes none of them, and undoing a fill puts the previous ones back. A widget has no attribution of its own: its field’s is the one.

Widgets#

A field shows on a page through a widget: the box you see and click. Most fields have one. A radio group is one field with a widget per button, and a field repeated on every page (a reference number in the header, say) has a widget per page, all showing the same value.

Each widget in field.widgets has its page and its ref. The form also lists every widget in widgets: where it is (rect, in points from the page’s top-left), how it looks, its state, what it does when clicked (actions), and the ref of the field it belongs to (field). So one read is enough to draw your own form layer over a page:

const { widgets } = await doc.forms.list();
 
for (const widget of widgets) drawBox(widget.page, widget.rect, widget.field);

Checkbox and radio widgets in field.widgets also have their exportValue (what the form data holds when this widget is checked) and checked.

A form field’s widgets come with the form, never with the annotations: page.annotations.list() doesn’t return them, and reading them takes doc.forms.read, not doc.annotate.read. Each page’s widget images, every look and state, come from page.forms.renderAppearances().

Find a field#

The ref from list() is the best address: it keeps working when the field is renamed. When you know a field by its name instead (from your backend, or from data you stored), make a ref from the name:

import { toFieldRef } from '@embedpdf/engine';
 
const field = await doc.forms.get(toFieldRef('billing.name'));

The name is the full name, with dots between the groups. A field that isn’t there fails with NotFound.

Fill in a field#

setValue() takes the field’s ref and its new value, in the same fields a read returns. What you read, you can write back:

await doc.forms.setValue(name.ref, { value: 'Ada Lovelace' }); // text
await doc.forms.setValue(agree.ref, { checked: true }); // checkbox
await doc.forms.setValue(plan.ref, { value: 'yearly' }); // radio group
await doc.forms.setValue(country.ref, { value: 'NL' }); // dropdown
await doc.forms.setValue(toppings.ref, { selectedValues: ['cheese', 'olives'] }); // list
FamilyValue
text{ value }; null empties it
checkbox{ checked }
radio{ value }: the export value of the button to choose; 'Off' or null clears
combobox{ value }: an option value; null clears
listbox{ selectedValues }: option values; [] clears

setValue() returns the field as it is now:

const { field } = await doc.forms.setValue(name.ref, { value: 'Ada' });

The engine redraws the field’s widgets, so the page shows the new value in every PDF app. Render the page again to see it.

What the engine checks#

The engine checks a value before it writes anything. A call that fails with InvalidArg leaves the field as it was:

  • The value must be the family’s own: { checked: true } on a text field fails.
  • A dropdown or list takes option values, not labels. A dropdown with edit: true also takes free text. More than one value needs a list with multiSelect.
  • A radio group’s value must be the export value of one of its buttons.
  • A radio group with noToggleToOff can’t be cleared once a button is chosen.
  • Push buttons and signature fields hold no value you can set.

Some things are fixed up rather than refused: text longer than maxLength is cut to fit, and in a radio group with radiosInUnison, the buttons that share an export value check together.

readOnly is an instruction for PDF apps: they don’t let the user change the field. The engine still accepts your own writes, for example a total your code calculates.

Reset#

reset() puts back each field’s defaultValue, or empties it when it has none. Without an argument it resets the whole form; pass a ref, or a list of refs, to reset only those fields:

await doc.forms.reset(); // every field
await doc.forms.reset(name.ref);
await doc.forms.reset([name.ref, email.ref]);

It returns the fields it changed, as they are now. A field that already holds its default is left alone and isn’t in the list:

const { fields } = await doc.forms.reset();
 
console.log(`${fields.length} fields reset`);

Fields that hold no value, such as push buttons and signature fields, are skipped. Resetting the whole form also skips the fields a signature locked and fields stored inline; name one of those yourself and the call fails, the same as a setValue() would.

Move fields and values between documents#

export() takes fields out as one bundle, each with its value. importValues() fills another document’s fields of the same name from it, and import() copies the fields themselves:

const bundle = await doc.forms.export();
 
const { fields, dropped } = await other.forms.importValues(bundle);

Each value is checked like a setValue(). A field the document doesn’t have, a value it can’t take, or a field a signature locked is left out and listed in dropped; the rest still land, as one change you can undo. See Import & export.

Events#

setValue(), reset() and importValues() fire forms.valueSet, one per field that changed. Each carries what the call returned for that field:

doc.events.on('forms.valueSet', ({ field }) => {
  updateSummary(field);
});

When one call changes several fields, such as reset() or importValues(), their events share origin.tx, like an annotation import.

Permissions#

Reading the form needs doc.forms.read, and exporting it doc.download too. Filling in, resetting and importing values needs doc.forms.fill, which includes doc.forms.read. See Permissions.

A signed document can take filling away, for the whole form or for single fields. The call then fails with ProtectedDocument; see Digital signatures.

More#

  • Creating form fields: add fields and widgets, change and delete them, repair a broken form.
  • Import & export: copy fields from one document to another, or fill a form in from a bundle.
  • Digital signatures: sign a signature field and check the signatures in a document.
  • Changes & undo: fill in several fields as one action, and undo it.

Good to know#

Calculations, formatting and validation#

The engine doesn’t run the form’s JavaScript. setValue() stores the value you give it: it doesn’t recalculate a total, format a date or an amount, or run a validation script. PDF apps run those scripts while a user types.

A field’s scripts are in field.actions (keystroke, format, validate and calculate), and the list’s calculationOrder holds the fields whose totals are calculated, in order. To write them, see Scripts and calculations.

The viewer’s form plugin runs these scripts and writes their results as one change: forms.setValue, forms.reset, forms.setDisplay (whether the field shows and prints) and forms.setAppearanceText (the formatted text the field shows, while its value stays the same) ops, in the order the scripts asked for them. All of them are written, or none is: a field a signature locked refuses the whole change. You only need these ops when you run form scripts yourself.

Recovered fields#

Some PDFs have fields that show on the page but are missing from the form’s list of fields, a bug in the tool that made them. The engine finds them anyway: they come back with origin: 'recovered' and fill in like any other field. Other PDF apps may not see them; repair() fixes the file.

A field on several pages#

A field’s widgets can be on different pages. The meta of a setValue() or reset() result lists every widget whose look changed, with its page (meta.changedWidgets). For a radio group that’s the button switched off and the one switched on. Use it to render only the pages that changed.

meta.redrawnWidgets lists those whose picture the engine drew again, such as a text field’s widgets after a new value. A checkbox or radio button that switches shows another picture it already has, so it isn’t in the list.

Checkboxes with several widgets#

A checkbox can have several widgets, each with its own exportValue: a “Yes / No” pair, for example. { checked: true } checks the first widget; { value: 'No' } checks the widget with that export value.

In the file, each checkbox and radio widget also has an onState: the name of its checked look (PDF’s appearance state). It’s usually the same as the export value, and widgets the engine creates always use their exportValue for it. You never need it to fill a field in.

The field’s value in the file (valueEntry) is the checked widget’s onState, or 'Off', as the PDF standard says. Some tools write the widget’s position instead ('0', '1'); such a value still resets to that widget.

XFA forms#

formKind is 'xfa' when the PDF also carries an XFA form, an older XML form format. The engine works with the document’s regular fields and never runs XFA.

Fields stored inline#

A field that the file stores inside another object, rather than as an object of its own, can be read but not changed: writes fail with InvalidArg. PDF tools rarely write fields this way.

Was this page helpful?

Your feedback goes directly to the documentation team.