DEV Community

Cover image for I built xfa.js — the first open source XFA forms renderer for the web
Danux-Be
Danux-Be

Posted on

I built xfa.js — the first open source XFA forms renderer for the web

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",
//   ...
// }
Enter fullscreen mode Exit fullscreen mode

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)}
    />
  );
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)