| name | pdflib-syntax-forms |
| description | Use when working with PDF form fields in pdf-lib — reading, creating, or manipulating fields. Prevents the common mistake of not calling form.flatten() after filling, leaving forms editable when they should be locked. Covers getForm, all field types (text, checkbox, radio, dropdown), field properties, flattening. Keywords: getForm, PDFForm, PDFTextField, PDFCheckBox, PDFRadioGroup, PDFDropdown, flatten, read form fields, create form, add text field, add checkbox, interactive PDF form.
|
| license | MIT |
| compatibility | Designed for Claude Code. Requires pdf-lib 1.x with TypeScript/JavaScript. |
| metadata | {"author":"OpenAEC-Foundation","version":"1.0"} |
pdf-lib Form Fields: Syntax Reference
Quick Reference
import { PDFDocument } from 'pdf-lib'
const pdfDoc = await PDFDocument.load(existingPdfBytes)
const form = pdfDoc.getForm()
const fields = form.getFields()
fields.forEach(f => console.log(f.getName(), f.constructor.name))
const textField = form.getTextField('Full Name')
const checkbox = form.getCheckBox('Agree')
const radio = form.getRadioGroup('Priority')
const dropdown = form.getDropdown('Country')
const optionList = form.getOptionList('Languages')
const button = form.getButton('Logo')
const maybeMissing = form.getFieldMaybe('MightNotExist')
textField.setText('John Doe')
checkbox.check()
radio.select('High')
dropdown.select('Netherlands')
optionList.select(['TypeScript', 'Python'])
form.flatten()
const pdfBytes = await pdfDoc.save()
Critical Warnings
CASE-SENSITIVE FIELD NAMES: Field names are ALWAYS case-sensitive. form.getTextField('fullName') and form.getTextField('FullName') are DIFFERENT fields. ALWAYS use form.getFields().map(f => f.getName()) to discover exact field names before accessing them. A single wrong character causes a throw.
DEFAULT FONT LIMITATION: The default form font (Helvetica) ONLY supports Latin characters (WinAnsi encoding). For ANY non-Latin text (accented characters, CJK, Arabic, Cyrillic), you MUST register fontkit, embed a custom font, and call form.updateFieldAppearances(customFont). Failure to do this produces garbled output or throws.
XFA FORMS: Some PDFs use XFA (XML Forms Architecture) instead of AcroForm. pdf-lib does NOT support XFA. ALWAYS call form.deleteXFA() before working with fields from XFA-based PDFs — this forces fallback to AcroForm fields.
FLATTEN IS ALL-OR-NOTHING: form.flatten() flattens ALL fields. There is no built-in selective flattening. To flatten specific fields only, use form.removeField(field) on the fields you want to keep interactive BEFORE calling form.flatten().
Essential Patterns
Pattern 1: Fill an Existing Form
import { PDFDocument } from 'pdf-lib'
const pdfDoc = await PDFDocument.load(formPdfBytes)
const form = pdfDoc.getForm()
const fieldNames = form.getFields().map(f => f.getName())
console.log(fieldNames)
form.getTextField('CharacterName 2').setText('Mario')
form.getCheckBox('Check Box3').check()
form.getRadioGroup('Group2').select('Choice1')
form.getDropdown('Dropdown7').select('Infinity')
const marioImage = await pdfDoc.embedPng(marioImageBytes)
form.getButton('CHARACTER IMAGE').setImage(marioImage)
form.flatten()
const pdfBytes = await pdfDoc.save()
Pattern 2: Create a New Form
import { PDFDocument } from 'pdf-lib'
const pdfDoc = await PDFDocument.create()
const page = pdfDoc.addPage([550, 750])
const form = pdfDoc.getForm()
const nameField = form.createTextField('user.name')
nameField.setText('Default Name')
nameField.addToPage(page, { x: 50, y: 650, width: 200, height: 30 })
const agreeField = form.createCheckBox('user.agree')
agreeField.addToPage(page, { x: 50, y: 600, width: 20, height: 20 })
agreeField.check()
const priorityField = form.createRadioGroup('user.priority')
priorityField.addOptionToPage('High', page, { x: 50, y: , : , : })
priorityField.(, page, { : , : , : , : })
priorityField.()
countryField = form.()
countryField.([, , ])
countryField.()
countryField.(page, { : , : , : , : })
langField = form.()
langField.([, , ])
langField.()
langField.(page, { : , : , : , : })
pdfBytes = pdfDoc.()
Pattern 3: Non-Latin Text in Form Fields
import { PDFDocument } from 'pdf-lib'
import fontkit from '@pdf-lib/fontkit'
const pdfDoc = await PDFDocument.load(formPdfBytes)
pdfDoc.registerFontkit(fontkit)
const customFont = await pdfDoc.embedFont(unicodeFontBytes)
const form = pdfDoc.getForm()
form.getTextField('name').setText('日本語テスト')
form.updateFieldAppearances(customFont)
const pdfBytes = await pdfDoc.save()
Pattern 4: Make Fields Read-Only Without Flattening
const form = pdfDoc.getForm()
const fields = form.getFields()
fields.forEach(field => field.enableReadOnly())
Pattern 5: XFA Form Handling
const pdfDoc = await PDFDocument.load(xfaPdfBytes)
const form = pdfDoc.getForm()
if (form.hasXFA()) {
form.deleteXFA()
}
const fields = form.getFields()
Decision Tree: Field Type Selection
What kind of input do you need?
├── Free text input? → createTextField('name')
│ ├── Single line? → default (no extra config)
│ ├── Multiline? → field.enableMultiline()
│ ├── Password? → field.enablePassword()
│ └── Fixed-width cells? → field.enableCombing() + field.setMaxLength(n)
├── Yes/No toggle? → createCheckBox('name')
├── One choice from several? (mutually exclusive)
│ ├── Small set (2-5)? → createRadioGroup('name')
│ └── Large set (6+)? → createDropdown('name')
├── Multiple choices from a list?
│ ├── Compact display? → createDropdown('name') + enableMultiselect()
│ └── Visible list? → createOptionList('name') + enableMultiselect()
└── Image or button? → createButton('name')
Decision Tree: Form Font Selection
Setting text in form fields?
├── Latin characters only (ASCII)?
│ └── Default Helvetica works — no extra setup needed
├── Extended Latin (accents, diacritics)?
│ ├── Register fontkit
│ ├── Embed a Unicode-supporting font (TTF/OTF)
│ └── Call form.updateFieldAppearances(customFont)
└── Non-Latin (CJK, Arabic, Cyrillic)?
├── Register fontkit (REQUIRED)
├── Embed font that supports the target script
└── Call form.updateFieldAppearances(customFont)
Decision Tree: Form Finalization
Done filling the form?
├── Make permanently read-only (static PDF)?
│ └── form.flatten()
├── Keep interactive but locked?
│ └── fields.forEach(f => f.enableReadOnly())
├── Need selective flattening?
│ └── Remove fields to KEEP interactive, then form.flatten()
└── Keep fully editable?
└── Just save — do nothing extra
PDFForm API Summary
| Method | Returns | Throws |
|---|
pdfDoc.getForm() | PDFForm | Never |
form.getFields() | PDFField[] | Never |
form.getField(name) | PDFField | If not found |
form.getFieldMaybe(name) | PDFField | undefined | Never |
form.getTextField(name) | PDFTextField | If not found or wrong type |
form.getCheckBox(name) | PDFCheckBox | If not found or wrong type |
form.getRadioGroup(name) | PDFRadioGroup | If not found or wrong type |
form.getDropdown(name) | PDFDropdown | If not found or wrong type |
form.getOptionList(name) | PDFOptionList | If not found or wrong type |
form.getButton(name) | PDFButton | If not found or wrong type |
form.getSignature(name) | PDFSignature | If not found or wrong type |
form.createTextField(name) | PDFTextField | If name already exists |
form.createCheckBox(name) | PDFCheckBox | If name already exists |
form.createRadioGroup(name) | PDFRadioGroup | If name already exists |
form.createDropdown(name) | PDFDropdown | If name already exists |
form.createOptionList(name) | PDFOptionList |
Common Field Properties (All Field Types)
| Method | Purpose |
|---|
field.getName() | Get fully qualified field name |
field.enableReadOnly() | Prevent user modification |
field.disableReadOnly() | Allow user modification |
field.enableRequired() | Mark as required |
field.disableRequired() | Mark as optional |
field.enableExporting() | Include in form data export |
field.disableExporting() | Exclude from form data export |
field.isReadOnly() | Check read-only state |
field.isRequired() | Check required state |
field.isExported() | Check export state |
field.needsAppearancesUpdate() | Check if visual refresh needed |
Reference Links