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
DisplayDocTitleviewer 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:
tsimport { 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:
tsawait 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
altattributes on<img>tags. Decorative images needalt=""and ideallyrole="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:
bashverapdf --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:
tsimport { 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,