PDF Metadata and Accessibility: What the EAA Means for Your Documents

The European Accessibility Act takes effect in June 2025, and it applies to the PDFs your backend generates. Here's what developers need to know about tagged PDFs, metadata, and PDF/UA compliance.

pdfaccessibilityeaapdf-uacompliancenode.js

PDF Metadata and Accessibility: What the EAA Means for Your Documents

If you generate PDFs as part of a product sold in the EU — invoices, contracts, statements, tickets, reports — the European Accessibility Act (EAA) now applies to you. It came into force on June 28, 2025, and it treats PDFs the same way it treats websites and mobile apps: they need to be perceivable, operable, understandable, and robust.

This post is a practical breakdown of what that means at the file level, what metadata you actually need to set, and how to generate compliant PDFs without rewriting your entire document pipeline.

What the EAA actually requires for PDFs

The EAA itself doesn't say "your PDF must contain a /StructTreeRoot object." It points to harmonized standards. For PDFs, that means PDF/UA-1 (ISO 14289-1) and the WCAG 2.1 AA criteria applied to PDF content.

In concrete terms, a compliant PDF needs:

  • Tagged structure — every piece of content (headings, paragraphs, tables, lists, figures) has a semantic tag in a structure tree.
  • Logical reading order — tags appear in the order a screen reader should read them, not in visual layout order.
  • Document metadata — title, language, and a DisplayDocTitle viewer preference.
  • Alt text for images and figures.
  • Real text, not rasterized text. Scanned images of text aren't acceptable on their own.
  • Unicode mapping so copy/paste and screen readers get actual characters, not glyph IDs.
  • No reliance on color alone to convey information.

If you're using a headless browser or HTML-to-PDF pipeline, several of these are easy to get wrong by default.

The metadata you must set

Even before tackling structure, get the metadata right. Most accessibility checkers fail PDFs at this stage first.

The four critical entries:

txt
/Title — human-readable document title /Lang — document language (e.g., "en-US", "de-DE") ViewerPreferences /DisplayDocTitle true XMP metadata — must include dc:title and pdfuaid:part = 1

In pdf-lib, that looks like:

ts
import { PDFDocument } from "pdf-lib"; const pdf = await PDFDocument.create(); pdf.setTitle("Invoice INV-2025-0421"); pdf.setLanguage("en-US"); pdf.setAuthor("Acme GmbH"); pdf.setSubject("Customer invoice"); // DisplayDocTitle is not exposed directly; you set it via the catalog const catalog = pdf.catalog; catalog.set( pdf.context.obj({ DisplayDocTitle: true }).reference, // ...you'd typically write this into ViewerPreferences );

The DisplayDocTitle flag matters because without it, viewers show the filename instead of the title — which screen readers then announce. It's a one-line setting that fails a lot of audits.

Tagged PDFs are the hard part

Setting metadata is mechanical. Producing a tagged PDF from a generation pipeline is where most teams get stuck.

If you're using Puppeteer or Playwright with page.pdf(), you'll notice they produce untagged PDFs by default. Chromium supports tagged PDF generation, but you have to opt in:

ts
await page.pdf({ path: "invoice.pdf", tagged: true, outline: true, format: "A4", });

tagged: true instructs Chromium to emit a structure tree based on the HTML. This means your HTML semantics now matter for the PDF output:

  • Use <h1>–<h6> properly, not styled <div>s.
  • Use <table> with <thead>, <th scope="col">, and <tbody>.
  • Put real alt attributes on <img> tags. Decorative images need alt="" and ideally role="presentation".
  • Use <ul>/<ol> for lists, not bullet characters in paragraphs.
  • Set <html lang="en"> so the PDF inherits a language.

If you've been generating PDFs from soup-of-divs templates, this is the work: refactor templates to use semantic HTML.

Verifying compliance

Don't ship and hope. There are two tools every PDF pipeline should run against output:

bash
# PAC 2024 (PDF Accessibility Checker) — GUI, very strict against PDF/UA # https://pdfua.foundation/en/pdf-accessibility-checker-pac/ # veraPDF — CLI, integrates into CI verapdf --flavour ua1 invoice.pdf

veraPDF returns a non-zero exit code on failure, which makes it easy to wire into a GitHub Actions step or a pre-deploy check:

bash
verapdf --flavour ua1 --format json out/*.pdf > report.json

Treat PDF/UA failures the same way you treat failing tests.

Where Kamy fits

If you'd rather not maintain a Chromium build, a tagging pipeline, and a veraPDF gate, this is the kind of thing Kamy handles for you. You send a template and data; the API returns a tagged PDF with metadata set correctly:

ts
import { kamy } from "@kamydev/sdk"; const pdf = await kamy.documents.render({ template: "invoice", data: { invoiceNumber: "INV-2025-0421", customer: "Acme GmbH" }, options: { tagged: true, pdfUA: true,