The problem nobody solved
If you've ever opened a government PDF form on Mac or Linux and seen this:
"Please wait... If this message is not eventually replaced by the proper
contents of the document, your PDF viewer may not be able to display
this type of document."
You've met XFA.
XFA (XML Forms Architecture) is Adobe's format for dynamic PDF forms.
Unlike regular AcroForm PDFs, XFA forms are essentially XML applications
embedded inside a PDF shell — with JavaScript logic, dynamic sections,
repeatable fields, conditional show/hide behavior, and calculated values.
Governments and militaries worldwide still use XFA for critical documents:
- 🇺🇸 DS-7801 — US Overseas Vetting Questionnaire (State Department)
- 🇺🇸 SF-86 — National Security Clearance Questionnaire (OPM)
- 🇬🇧 NSV forms — UK Security Vetting
- 🇫🇷 NIS — French security clearance (DRSD)
- 🌍 NATO equivalents across member states
The problem? No browser can render XFA forms natively. Not Chrome,
not Firefox, not Safari. The only working solution has always been
desktop Adobe Acrobat on Windows.
How we got here
The history of XFA on the web is a graveyard of abandoned attempts:
- Adobe dropped XFA from their own PDF Embed API — their official documentation lists XFA as explicitly unsupported
- Mozilla added partial XFA support to pdf.js around 2017, then removed it entirely in 2021, citing the impossibility of maintaining compatibility with a 1500-page proprietary spec
- PSPDFKit and Apryse support XFA but charge $500+/month — not an option for most organizations
- Adobe Experience Manager converts XFA to HTML5 on the fly, but it's an enterprise platform costing tens of thousands per year
The result: organizations that need to collect XFA forms from people
are stuck telling them "please install Adobe Acrobat on a Windows machine".
In 2026.
What changed: AI as a co-developer
When Mozilla abandoned XFA in 2021, implementing the spec required
developers to manually read 1500 pages of Adobe documentation and
hand-write every edge case. The effort/result ratio was brutal.
Today, AI changes that equation entirely. With Claude as a co-developer:
- Analyze any XFA file and understand its full structure in seconds
- Generate parser code that handles edge cases from the spec
- Iterate on the scripting engine rapidly
- Debug rendering issues with context the original pdf.js team didn't have
This is what made xfa.js possible now, when it wasn't practical before.
What we built
xfa.js is an open source TypeScript library — MIT licensed —
that aims to be the definitive XFA implementation for the web.
Phase 1: Parser ✅
The parser extracts the complete form structure from any XFA PDF.
import { XfaParser } from '@quorbe/xfa.js';
const parser = new XfaParser();
const document = await parser.parseFromBuffer(pdfBuffer);
console.log(document.metadata.totalFields); // 579
console.log(document.sections.length); // 20+
console.log(document.fields[0]);
// {
// name: "s_last",
// label: "Last Name",
// path: "ovq.main.forms.s_name.s_last",
// type: "TEXT",
// required: true,
// sectionId: "s_name",
// ...
// }
Validated on the DS-7801 (US Overseas Vetting Questionnaire):
| Metric | Result |
|---|---|
| Fields extracted | 579 |
| Field types detected | text, date, numeric, checkbox, radio, dropdown, signature, button, barcode, textMultiline |
| Scripts extracted | 247 |
| Section hierarchy | ✅ Full tree with parent/child relationships |
| Repeatable sections | ✅ Detected with min/max occurrence |
| Captions | 567 correctly extracted |
| Option lists | 212 (for dropdowns and radio groups) |
Phase 2: Renderer ✅
The renderer turns the parsed XfaDocument into a fully interactive
React form.
import { XfaParser } from '@quorbe/xfa.js';
import { XfaForm } from '@quorbe/xfa.js/renderer';
function MyForm({ pdfBuffer }: { pdfBuffer: ArrayBuffer }) {
const [doc, setDoc] = useState(null);
useEffect(() => {
const parser = new XfaParser();
parser.parseFromBuffer(pdfBuffer).then(setDoc);
}, [pdfBuffer]);
if (!doc) return Parsing...;
return (
<XfaForm
document={doc}
onSubmit={(values) => console.log(values)}
/>
);
}
What the renderer handles:
- ✅ All field types (text, date, checkbox, radio, dropdown, multiline)
- ✅ Repeatable sections with +/− buttons (aliases, addresses, employment history)
- ✅ Conditional show/hide logic driven by field values
- ✅ XFA scripting engine emulating the core object model:
-
xfa.resolveNode("path").rawValue— read/write field values -
xfa.resolveNode("path").presence— show/hide sections -
xfa.event.newText— capture input during change events -
xfa.host.messageBox()— validation alerts -
xfa.form.recalculate()— trigger dependent calculations
-
- ✅ Picture mask validation (
date{MM/DD/YYYY}) - ✅ Error display on blur/submit
- ✅ Zero CSS framework dependencies — bring your own styles
Phase 3: Writer 🚧
Coming next: write filled values back into the XFA datasets stream
and generate a completed PDF for download.
The architecture
@quorbe/xfa.js
├── parser/
│ ├── extract.ts # Pull XFA packets from PDF (/Root > /AcroForm > /XFA)
│ ├── xml.ts # Ordered DOM wrapper (sibling order matters in XFA)
│ ├── template.ts # Walk tree, extract elements
│ ├── datasets.ts # Merge current values from datasets stream
│ └── XfaParser.ts # Public API
├── renderer/
│ ├── XfaForm.tsx # Root component
│ ├── XfaSection.tsx # Section + children
│ ├── XfaField.tsx # Field dispatcher
│ ├── inputs/ # TextField, DateField, CheckboxField, RadioGroup...
│ ├── RepeatableSection.tsx
│ └── hooks/
│ ├── useXfaForm.ts # Form state
│ ├── useScripting.ts # XFA JS engine
│ └── useVisibility.ts # Show/hide logic
├── scripting/ # XFA scripting object model
└── writer/ # Phase 3 — PDF generation
Install
npm install @quorbe/xfa.js
Peer dependencies: React 18+
The road ahead
The XFA spec is 1500+ pages. We're not done — but the hardest parts
(parsing and basic rendering) are working on real government forms.
What's next:
- Phase 3 — Writer: generate filled PDFs from submitted values
- FormCalc support — some older XFA forms use FormCalc instead of JavaScript
- More government forms — SF-86, UK NSV, French NIS
- Better scripting coverage — edge cases in xfa.resolveNode path resolution
Contributing
This is exactly the kind of project that benefits from people who have
dealt with XFA pain in the real world.
If you work with XFA forms in government, defense, security, or enterprise —
we want to hear from you. Open an issue, submit a PR, or just share
an XFA file that breaks the parser.
GitHub: https://github.com/quorbe/xfa.js
npm: npm install @quorbe/xfa.js
License: MIT
Top comments (0)