The European Accessibility Act now covers PDFs your API generates. If EU users download invoices, tickets, or reports from your product, those documents must be accessible tagged PDFs. Most HTML-to-PDF pipelines produce untagged output by default. Here is how to fix that, what Chromium's print pipeline actually gives you, and how to audit the result.
That means any PDF your system generates and makes available to users inside the EU falls under the same accessibility requirements as your web interface.
EN 301 549 covers non-web documents, and PDFs are firmly in scope. Invoices, bank statements, tickets, and reports downloaded by EU users all need to comply.
The relevant WCAG 2.2 success criteria for PDFs include:
Your PDF generator is not exempt because it runs headless. A user who receives an untagged invoice via email and opens it in a screen reader has the same legal right to access as someone using your web dashboard.
A tagged PDF contains a tag tree that maps content to logical structure, not just visual position on the page. This tree is what screen readers use to navigate and announce content correctly.
Headings must be actual H1-H6 tags in the underlying structure, not styled divs that merely look like headings. A screen reader user navigating by heading level will skip right past a <div class="h1"> because it has no semantic meaning.
Tables need <th> elements with scope="col" or scope="row" attributes. Without these, a screen reader reads tables as flat sequences of cells, forcing users to count columns mentally to understand which header applies.
Images need alt text. Decorative elements should be marked as artifacts so screen readers ignore them entirely. The document language must be set via the lang attribute on the HTML root, which carries through to the PDF's language declaration.
Reading order follows the tag tree, not the visual top-to-left flow. A sidebar positioned visually on the right but earlier in the DOM will be read before the main content unless the tag tree corrects this. Link text must be meaningful—"click here" repeated five times is useless when a screen reader lists all links on the page.
Default Chromium output is untagged. No structure tree, no headings, no table semantics. The PDF looks correct visually but contains no accessibility information.
Chrome 85 added alt text carryover to tagged PDFs [1]. The Chrome DevTools Protocol's Page.printToPDF exposes an experimental generateTaggedPDF boolean for embedding a tag tree, and a separate generateDocumentOutline boolean for bookmarks [2]. These are not the same thing. Bookmarks help navigation but do not make a PDF accessible.
generateTaggedPDF does not fix bad HTML semantics. A styled div stays a styled div in the tag tree. The flag only carries over what is already present in well-formed markup. Headers and footers printed via CSS or template injection become content streams in the PDF body, not artifacts, which means screen readers may announce page numbers and repeated logos on every single page. Font subsetting and embedding vary by Chromium version and flags, so the same HTML can produce different accessibility results across environments.
You control accessibility at the markup level.
Use real <h1> through <h6> tags. Never style a <div> to look like a heading. The tag tree copies the DOM structure, and screen readers offer heading navigation as a primary way to move through long documents.
Build tables with <th>, <td>, and explicit scope="col" or scope="row" attributes. Complex tables with nested headers need id and headers attributes, but most generated documents never reach that complexity.
Add alt text to every <img> tag. Hide decorative images with role="presentation" or empty alt="". Set the document language on the HTML root:
<html lang="en">
Ensure 4.5:1 contrast ratio on all text. Automated tools catch this easily, but designers often miss it on subtle gray footer text. Avoid images of text entirely. Use real text with web fonts so it remains selectable, searchable, and scalable.
Write meaningful link text. "Download your invoice for March 2026" is accessible. "Click here" is not.
Keep headers and footers simple. Repeated content like page numbers should be marked as artifacts where the renderer supports it. Test with @media print rules since Chromium re-lays the entire layout at paper width, which can reflow content and alter reading order.
PAC 2026 checks PDF/UA and WCAG requirements and includes AI-assisted checks that reduce manual testing time [3]. It is funded by the German Federal Ministry of Labor and Social Affairs and has been tried and tested since 2010. Run every template variant through PAC before production deployment.
veraPDF validates PDF/A and structural compliance and can run in CI pipelines. Add it to your build process so regressions fail the build automatically.
Perform screen-reader spot checks with NVDA or JAWS. Navigate by headings. Read tables cell by cell. Verify that the reading order makes sense and that decorative elements are silent.
Inspect the tag tree directly in Adobe Acrobat Pro or Foxit PDF Editor. Look for heading levels that skip logically, tables without header cells, and images missing alternate descriptions.
Automated checks catch structural errors but not reading-order logic. A table may pass structural validation yet read columns in the wrong sequence if the visual layout and DOM order diverge. Test one sample per template variant rather than every individual document. The template determines the tag structure; data changes do not.
MarkupGo's PDF API accepts HTML, URL, template, or markdown input [4]. To enable accessible output, set pdfUA: true in the API options [4].
const markupgo = new MarkupGo(API_KEY);
const options = {
properties: {
size: {
width: 210,
height: 297
},
margins: {
top: 10,
bottom: 10,
left: 10,
right: 10
}
},
pdfUA: true,
emulatedMediaType: 'print'
};
// HTML source
fetch('/api/v1/pdf', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': 'YOUR_API_KEY'
},
body: JSON.stringify({
source: {
type: 'html',
data: `<html lang="en">
<head>
<style>
body { font-family: system-ui, sans-serif; }
h1 { color: #111; }
th { background: #eee; }
td, th { padding: 8px; border: 1px solid #ccc; }
</style>
</head>
<body>
<h1>Invoice #2026-1042</h1>
<table>
<thead>
<tr>
<th scope="col">Item</th>
<th scope="col">Quantity</th>
<th scope="col">Price</th>
</tr>
</thead>
<tbody>
<tr>
<td>Consulting hours</td>
<td>12</td>
<td>€1,200.00</td>
</tr>
</tbody>
</table>
<p>Total: <strong>€1,200.00</strong></p>
</body>
</html>`
},
options
})
})
Use real heading and table markup in your template HTML. Set the lang attribute at the document root. For custom headers and footers, provide complete HTML documents without JavaScript [4]. The header and footer injection supports pageNumber and totalPages classes for dynamic pagination [4].
Pass dynamic data via template variables or Magic Template URL query parameters [5]. A no-code invoice URL uses your template ID and passes data in the query string:
// see Markupgo's API reference for Magic Template URL structure
Before shipping to production, verify output with PAC 2026. Fix any structural errors it reports, then spot-check with a screen reader.
The pdfUA option enables PDF for Universal Access tagging in the output [4]. It tells the rendering engine to embed a tag tree based on the HTML structure.
It does not rewrite your HTML semantics. Bad markup produces bad tags. A document full of styled divs will generate a flat tag tree with no headings, no table structure, and no logical reading order.
A consistent Chrome engine version helps avoid tag-generation differences across environments. Self-managed Puppeteer installations drift with system updates, Docker base image changes, and flag variations. Hosted rendering removes serverless cold-start issues, CORS failures, and font variability that can silently alter document structure.
Tagging quality depends entirely on template markup, not just the renderer. Run PAC 2026 on your output, fix the HTML, and regenerate until the report is clean.
Start by auditing one of your existing generated PDFs in PAC 2026 today. If it fails, trace the error back to the template markup, enable pdfUA: true, and test again before the next production deploy.