PDF metadata and accessibility: what the EAA means for your documents

The European Accessibility Act takes effect in June 2025 and it covers PDFs too. Here's what developers generating documents programmatically actually need to do.

pdfaccessibilityeaapdf-uacompliancenode.js

PDF metadata and accessibility: what the EAA means for your documents

The European Accessibility Act (EAA) became enforceable on June 28, 2025. If your product serves EU consumers and generates PDFs — invoices, contracts, statements, tickets, reports — those PDFs are now in scope. "In scope" means they need to be readable by assistive technology, properly tagged, and carry correct metadata.

Most PDFs generated by backend services today fail this. They're rendered from HTML with headless Chrome or built with a layout library that treats the output as a visual bitmap of text runs. Screen readers see garbage. This post covers what the EAA actually requires for PDFs, what to put in your pipeline, and what to check before you ship.

What the EAA actually requires for PDFs

The EAA references EN 301 549, which in turn references WCAG 2.1 AA and PDF/UA-1 (ISO 14289-1) for PDF documents. In practice, a compliant PDF needs:

  • A tagged structure tree (headings, paragraphs, lists, tables, figures) that mirrors the logical reading order.
  • Document metadata: title, language, and a DisplayDocTitle viewer preference so the title (not the filename) is shown in the window chrome.
  • Alternative text for images and non-text content.
  • Correct reading order independent of visual layout.
  • Unicode mapping for all glyphs, so text can be extracted and read aloud.
  • No reliance on color alone to convey information, and sufficient contrast (WCAG AA: 4.5:1 for body text).
  • A declared PDF/UA identifier in the XMP metadata if you want to claim PDF/UA conformance.

The one most developers miss is the structure tree. Without it, a PDF is just drawing commands on a page, and nothing — not NVDA, not JAWS, not VoiceOver — can interpret it as a document.

Metadata: the easy part, usually done wrong

Every PDF should have at minimum a title, language, and author set in the Document Information Dictionary and mirrored in XMP. Here's what good metadata looks like inside a PDF (viewed via pdfinfo or exiftool):

txt
Title: Invoice INV-2025-00042 Author: Acme GmbH Subject: Invoice for order #4471 Language: de-DE PDF version: 1.7 Tagged: yes UA conformance: PDF/UA-1

If you're using pdf-lib directly, you can set the Info dictionary like this:

ts
import { PDFDocument } from "pdf-lib"; const doc = await PDFDocument.create(); doc.setTitle("Invoice INV-2025-00042"); doc.setAuthor("Acme GmbH"); doc.setSubject("Invoice for order #4471"); doc.setLanguage("de-DE"); doc.setCreator("[email protected]");

This handles the Info dictionary, but it does not make the PDF tagged, and it does not set DisplayDocTitle. For true compliance you need to go further.

The hard part: structure and tags

A tagged PDF has a parallel tree describing semantics:

txt
Document ├── H1 "Invoice INV-2025-00042" ├── P "Issued on 15 November 2025" ├── Table │ ├── TR (header row) │ │ ├── TH "Description" │ │ ├── TH "Qty" │ │ └── TH "Amount" │ └── TR (data row) │ ├── TD "Consulting services" │ ├── TD "10" │ └── TD "€1,500.00" └── P "Thank you for your business"

If you're rendering from HTML, the browser needs to emit this tree. Chromium's print pipeline supports tagged PDFs when you pass the right flag, but the tagging quality depends heavily on your HTML. Use semantic elements (<h1>, <table>, <th>, <ul>), set lang on <html>, and provide alt on every <img>.

With Puppeteer:

ts
const pdf = await page.pdf({ format: "A4", tagged: true, outline: true, printBackground: true, });

That gets you a tagged tree, but you still need to post-process to add DisplayDocTitle, declare PDF/UA in XMP, and verify tag correctness.

A reasonable pipeline

If you own the full stack, the pattern that works:

  1. Author templates in semantic HTML with proper headings, tables, and alt text.
  2. Render to a tagged PDF.
  3. Post-process to inject metadata, viewer preferences, and the PDF/UA XMP marker.
  4. Validate with an automated checker before serving.

For step 4, the open-source veraPDF is the standard. You can run it in CI:

bash
verapdf --flavour ua1 --format text invoice.pdf

A passing document returns exit code 0 and no failed rules. Wire this into your test suite so a template change can't silently break accessibility.

Where Kamy fits

Kamy generates tagged PDFs by default and sets the required metadata (title, language, DisplayDocTitle, PDF/UA XMP) based on your template definition. You pass semantic data and a locale, and the output passes veraPDF's PDF/UA-1 checks without extra work:

ts
import { kamy } from "@kamydev/sdk"; const pdf = await kamy.documents.render({ template: "invoice", locale: "de-DE", metadata: { title: "Invoice INV-2025-00042", author: "Acme GmbH",