PDF is one of the most widely distributed document formats — invoices, reports, manuals, application forms — yet most PDFs are unusable by people who rely on screen readers or keyboard-only navigation.
Accessibility is no longer optional. Regulations in the US, Europe, and Japan impose legal obligations on organizations that publish inaccessible documents, and enforcement is increasing. An inaccessible PDF published to a public-facing website is a legal and reputational risk.
This guide covers everything you need to know: the relevant laws, the PDF/UA and WCAG standards, how to write accessible HTML templates, how to generate accessible PDFs with the FUNBREW PDF API, a verification checklist, and how to test compliance with free tools.
If you are new to HTML-to-PDF generation, start with the HTML to PDF complete guide. For CSS layout best practices, see the HTML to PDF CSS tips guide.
1. Why PDF Accessibility Matters
Legal Requirements
ADA (Americans with Disabilities Act) — United States
Under ADA Title III, businesses that operate public accommodations must ensure that digital content — including PDFs — is accessible to people with disabilities. Federal agencies are additionally subject to Section 508 of the Rehabilitation Act, which mandates WCAG 2.0 Level AA compliance for all electronic and information technology.
ADA-related accessibility lawsuits have grown significantly since 2015. Courts have found in multiple cases that inaccessible PDFs published on public websites constitute a violation.
Act for Eliminating Discrimination against Persons with Disabilities — Japan
The April 2024 amendment to Japan's disability discrimination law extended the obligation to provide "reasonable accommodation" to private businesses. Providing accessible digital documents — including PDFs — is increasingly interpreted as part of this obligation, particularly for services with public-facing functions or government-adjacent operations.
EAA (European Accessibility Act) — European Union
From June 2025, the EAA requires businesses selling certain products and services in EU markets to meet accessibility requirements. Digital documents including PDFs fall within scope for many affected organizations. WCAG 2.1 Level AA is the referenced technical standard.
Business Case for Accessibility
| Benefit | Detail |
|---|---|
| Larger audience | Around 15% of the global population (over 1 billion people) live with some form of disability |
| SEO improvement | Structured, semantic content improves search engine indexing |
| Better UX for everyone | Clear structure benefits mobile users, non-native readers, and slow connections |
| Brand reputation | Demonstrates commitment to social responsibility |
2. PDF/UA — The Accessibility Standard for PDFs
ISO 14289-1 (PDF/UA)
PDF/UA (Universal Accessibility) is the ISO standard (ISO 14289-1) that defines what makes a PDF accessible. "UA" stands for Universal Accessibility.
PDF/UA Core Requirements
├── Document Structure
│ ├── Must be a tagged PDF
│ ├── Logical reading order
│ └── Proper heading hierarchy (H1–H6)
├── Text
│ ├── Real text (not images of text)
│ ├── Language specified (lang attribute)
│ └── Abbreviations have expansions
├── Images and Figures
│ ├── All meaningful images have alternative text
│ └── Decorative images tagged as Artifact
├── Forms
│ ├── All form fields have labels
│ └── Tab order matches logical reading order
└── Color and Contrast
└── Sufficient contrast between text and background
WCAG 2.1 (Web Content Accessibility Guidelines)
WCAG is published by the W3C and defines accessibility requirements for web content. While not specifically a PDF standard, WCAG is referenced by most major accessibility laws when applied to digital documents.
WCAG 2.1 Level AA is the current benchmark accepted by most regulations and organizations.
The four POUR principles:
- Perceivable — Information can be seen or heard
- Operable — Content can be navigated with keyboard alone
- Understandable — Language is clear and consistent
- Robust — Content works with assistive technologies
3. Writing Accessible HTML Templates
When generating PDFs from HTML, accessibility must start in the HTML. FUNBREW PDF converts well-structured HTML into a tagged PDF that preserves the document structure. Bad HTML produces a bad PDF.
Use Semantic HTML
Choose HTML tags for their meaning, not their appearance.
<!-- Bad: structure built entirely with divs -->
<div class="title">PDF Accessibility Guide</div>
<div class="section-title">Introduction</div>
<div class="text">Accessibility is...</div>
<!-- Good: semantic tags that convey meaning -->
<h1>PDF Accessibility Guide</h1>
<h2>Introduction</h2>
<p>Accessibility is...</p>
Declare the Language
Specify the language of the document and mark any passages in a different language.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>PDF Accessibility Guide</title>
</head>
<body>
<h1>PDF Accessibility Guide</h1>
<!-- Mark foreign language passages -->
<p>See: <span lang="ja">障害者差別解消法</span> (Act for Eliminating Discrimination against Persons with Disabilities)</p>
</body>
</html>
Alt Text for Images
Every image needs an alt attribute. Decorative images get alt="" to tell assistive technologies to skip them.
<!-- Informative image -->
<img src="contrast-ratio-diagram.png"
alt="Contrast ratio comparison. Normal text requires 4.5:1, large text requires 3:1 minimum">
<!-- Decorative image (empty alt) -->
<img src="decorative-divider.png" alt="" role="presentation">
<!-- Complex chart (supplement with figcaption) -->
<figure>
<img src="q4-revenue-chart.png"
alt="Q4 2025 revenue bar chart. See caption below for data.">
<figcaption>
Q4 2025 monthly revenue: October $98K, November $112K, December $130K.
All months exceeded prior-year figures by more than 15%.
</figcaption>
</figure>
Accessible Table Markup
<!-- Accessible table with proper structure -->
<table>
<caption>Monthly Revenue Summary — Q4 2025</caption>
<thead>
<tr>
<th scope="col">Month</th>
<th scope="col">Revenue</th>
<th scope="col">Month-over-Month</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">October</th>
<td>$98,000</td>
<td>+8%</td>
</tr>
<tr>
<th scope="row">November</th>
<td>$112,000</td>
<td>+14%</td>
</tr>
<tr>
<th scope="row">December</th>
<td>$130,000</td>
<td>+16%</td>
</tr>
</tbody>
</table>
The scope="col" and scope="row" attributes help screen readers announce the header associated with each data cell.
Form Labels and ARIA Attributes
<form>
<div>
<label for="full-name">Full Name <span aria-label="required">*</span></label>
<input type="text"
id="full-name"
name="full-name"
required
aria-required="true"
aria-describedby="name-hint">
<p id="name-hint">Enter your legal name as it appears on your ID</p>
</div>
<div>
<label for="dob">Date of Birth</label>
<input type="date"
id="dob"
name="dob"
aria-label="Date of birth in YYYY-MM-DD format">
</div>
</form>
Color Contrast Requirements
WCAG contrast ratio minimums:
| Text Type | Minimum (AA) | Enhanced (AAA) |
|---|---|---|
| Normal text (under 18px) | 4.5:1 | 7:1 |
| Large text (18px and above) | 3:1 | 4.5:1 |
| UI components and graphics | 3:1 | — |
/* Bad: insufficient contrast */
body {
color: #999999; /* light gray */
background: #ffffff; /* white */
/* contrast ratio: 2.85:1 — fails WCAG AA */
}
/* Good: sufficient contrast */
body {
color: #333333; /* dark gray */
background: #ffffff; /* white */
/* contrast ratio: 12.63:1 — well above minimum */
}
h1, h2, h3 {
color: #1a1a2e; /* dark navy */
}
/* Don't rely on color alone — add underline to links */
a {
color: #0057b7;
text-decoration: underline;
}
4. Generating Accessible PDFs with FUNBREW PDF API
FUNBREW PDF is a Chromium-based HTML-to-PDF API. When you provide well-structured HTML, it generates a tagged PDF that preserves document structure for assistive technologies.
Try the playground to test HTML templates before integrating into your code.
curl — Basic Example
curl -X POST https://pdf.funbrew.cloud/api/v1/generate \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"html": "<!DOCTYPE html><html lang=\"en\"><head><meta charset=\"UTF-8\"><title>Accessible Report</title><style>body{font-family:sans-serif;color:#333;background:#fff;font-size:16px;line-height:1.6}h1{color:#1a1a2e;font-size:28px}h2{color:#1a1a2e;font-size:22px}table{width:100%;border-collapse:collapse}th,td{padding:8px;border:1px solid #666;text-align:left}th{background:#1a1a2e;color:#fff}</style></head><body><h1>Monthly Revenue Report</h1><h2>Summary</h2><p>Q4 2025 revenue summary.</p><table><caption>Monthly Revenue</caption><thead><tr><th scope=\"col\">Month</th><th scope=\"col\">Revenue</th></tr></thead><tbody><tr><th scope=\"row\">October</th><td>$98,000</td></tr></tbody></table></body></html>",
"options": {
"format": "A4",
"margin": { "top": "20mm", "right": "15mm", "bottom": "20mm", "left": "15mm" }
}
}' \
--output accessible-report.pdf
Python Implementation
import requests
import os
API_KEY = os.environ.get("FUNBREW_PDF_API_KEY")
API_URL = "https://pdf.funbrew.cloud/api/v1/generate"
HTML_TEMPLATE = """<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>{title}</title>
<style>
/* Accessibility-first styles */
body {{
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
color: #333333; /* contrast ratio vs white: 12.63:1 */
background: #ffffff;
font-size: 16px; /* minimum recommended font size */
line-height: 1.7; /* comfortable line height */
}}
h1 {{ font-size: 28px; color: #1a1a2e; margin-bottom: 8px; }}
h2 {{ font-size: 22px; color: #1a1a2e; margin-top: 32px; }}
h3 {{ font-size: 18px; color: #1a1a2e; margin-top: 24px; }}
/* Links: don't rely on color alone */
a {{
color: #0057b7;
text-decoration: underline;
}}
table {{
width: 100%;
border-collapse: collapse;
margin: 16px 0;
}}
th, td {{
padding: 10px 12px;
border: 1px solid #555555;
text-align: left;
}}
th {{
background: #1a1a2e;
color: #ffffff; /* contrast ratio: 16.1:1 */
font-weight: bold;
}}
</style>
</head>
<body>
<h1>{title}</h1>
<p>Generated: <time datetime="{date}">{date_display}</time></p>
<h2>Executive Summary</h2>
<p>{summary}</p>
<h2>Monthly Performance</h2>
<table>
<caption>{table_caption}</caption>
<thead>
<tr>
<th scope="col">Month</th>
<th scope="col">Revenue</th>
<th scope="col">Change</th>
</tr>
</thead>
<tbody>
{table_rows}
</tbody>
</table>
</body>
</html>"""
def generate_accessible_pdf(report_data: dict) -> bytes:
"""Generate an accessible report PDF."""
rows_html = ""
for row in report_data["monthly_data"]:
rows_html += f"""
<tr>
<th scope="row">{row['month']}</th>
<td>{row['revenue']}</td>
<td>{row['change']}</td>
</tr>"""
html = HTML_TEMPLATE.format(
title=report_data["title"],
date=report_data["date"],
date_display=report_data["date_display"],
summary=report_data["summary"],
table_caption=report_data["table_caption"],
table_rows=rows_html,
)
response = requests.post(
API_URL,
headers={
"Authorization": f"Bearer {API_KEY}",
"Content-Type": "application/json",
},
json={
"html": html,
"options": {
"format": "A4",
"margin": {
"top": "20mm",
"right": "15mm",
"bottom": "20mm",
"left": "15mm",
},
},
},
timeout=30,
)
response.raise_for_status()
return response.content
if __name__ == "__main__":
report = {
"title": "Q4 2025 Monthly Revenue Report",
"date": "2025-12-31",
"date_display": "December 31, 2025",
"summary": "All three months in Q4 exceeded prior-month figures.",
"table_caption": "Q4 2025 Monthly Revenue Data",
"monthly_data": [
{"month": "October", "revenue": "$98,000", "change": "+8%"},
{"month": "November", "revenue": "$112,000", "change": "+14%"},
{"month": "December", "revenue": "$130,000", "change": "+16%"},
],
}
pdf_bytes = generate_accessible_pdf(report)
with open("accessible-report.pdf", "wb") as f:
f.write(pdf_bytes)
print("PDF generated: accessible-report.pdf")
Node.js Implementation
import fetch from 'node-fetch';
import { writeFileSync } from 'fs';
const API_KEY = process.env.FUNBREW_PDF_API_KEY;
const API_URL = 'https://pdf.funbrew.cloud/api/v1/generate';
/**
* Generate an accessible application form PDF.
* @param {Object} formData - Form data
* @returns {Buffer} - PDF binary
*/
async function generateAccessibleFormPDF(formData) {
const html = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>${formData.title}</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Arial, sans-serif;
color: #222222;
background: #ffffff;
font-size: 16px;
line-height: 1.7;
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
h1 { font-size: 26px; color: #1a1a2e; border-bottom: 2px solid #1a1a2e; padding-bottom: 8px; }
h2 { font-size: 20px; color: #1a1a2e; margin-top: 28px; }
dl { margin: 16px 0; }
dt { font-weight: bold; color: #333333; margin-top: 12px; }
dd {
margin-left: 20px;
padding: 6px 10px;
background: #f5f5f5;
border-left: 3px solid #1a1a2e;
}
/* Notice box: don't rely on color alone — prefix with text label */
.notice {
background: #fff3cd;
border: 1px solid #ffc107;
border-left: 4px solid #e67e00;
padding: 12px 16px;
border-radius: 4px;
margin: 16px 0;
}
.notice::before {
content: "[Notice] ";
font-weight: bold;
color: #7a4000;
}
</style>
</head>
<body>
<h1>${formData.title}</h1>
<div class="notice" role="note" aria-label="Important notice">
This form was automatically generated. Please review all fields before signing and submitting.
</div>
<h2>Applicant Information</h2>
<dl>
<dt>Full Name</dt>
<dd>${formData.applicantName}</dd>
<dt>Application Number</dt>
<dd><code>${formData.applicationId}</code></dd>
<dt>Application Date</dt>
<dd><time datetime="${formData.applicationDate}">${formData.applicationDateDisplay}</time></dd>
</dl>
<h2>Application Details</h2>
<dl>
<dt>Type</dt>
<dd>${formData.type}</dd>
<dt>Reason</dt>
<dd>${formData.reason}</dd>
</dl>
</body>
</html>`;
const response = await fetch(API_URL, {
method: 'POST',
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
html,
options: {
format: 'A4',
margin: { top: '20mm', right: '20mm', bottom: '20mm', left: '20mm' },
},
}),
});
if (!response.ok) {
const error = await response.json();
throw new Error(`PDF generation error: ${error.message}`);
}
return Buffer.from(await response.arrayBuffer());
}
// Example usage
const formData = {
title: 'Parental Leave Application',
applicantName: 'Jane Smith',
applicationId: 'APP-2025-001234',
applicationDate: '2025-12-01',
applicationDateDisplay: 'December 1, 2025',
type: 'Parental Leave (first child)',
reason: 'Parental leave for first child, expected January 15, 2026',
};
const pdfBuffer = await generateAccessibleFormPDF(formData);
writeFileSync('application-form.pdf', pdfBuffer);
console.log('PDF generated: application-form.pdf');
5. Accessibility Checklist
Use this checklist when building or reviewing HTML templates. Test in the playground as you work through each item.
Heading Structure
- One
<h1>per document, representing the document title - Headings descend in order: H2 → H3 → H4 (no skipping from H1 to H3)
- Headings describe structure, not appearance
- Every major section has a heading
Text and Language
-
<html lang="en">(or the appropriate language code) is present - Language switches within the document use
langattribute on the element - First use of abbreviations uses
<abbr title="expansion">ABBR</abbr> - Minimum font size: 14px (16px recommended)
Images and Media
- All images have
altattributes - Decorative images have
alt=""androle="presentation" - Complex charts have captions or in-body descriptions
- Chart data is also available in table format where practical
Color and Contrast
- Normal text (under 18px) has contrast ratio of 4.5:1 or higher
- Large text (18px and above) has contrast ratio of 3:1 or higher
- Information is not conveyed by color alone (use icons, text, or patterns)
- Error states use icon and text in addition to red color
Tables
-
<caption>clearly describes the table content -
<thead>and<tbody>are separated - Header cells use
scope="col"orscope="row" - Complex tables use
headersattribute to link cells to multiple headers
Forms
- All input fields have a visible
<label> - Required fields have
aria-required="true" - Error messages reference the field using
aria-describedby - Tab order follows visual reading order
6. Testing Tools
PAC 2024 (PDF Accessibility Checker)
PAC 2024 is a free Windows application that validates PDFs against PDF/UA-1 and WCAG 2.1.
- Checks tag structure, reading order, and alternative text
- Visual preview of the tag tree
- Detailed error reports for each failure
Adobe Acrobat Pro — Accessibility Checker
Adobe Acrobat Pro includes a built-in accessibility checker.
Menu: Tools → Accessibility → Accessibility Check
Key checks it performs:
- Document tagging
- Alternative text on images
- Table structure
- Form field labels
- Logical reading order
axe-core for HTML Pre-Testing
Test HTML accessibility before generating the PDF. This catches most issues earlier and faster than testing the final PDF.
npm install -g axe-cli
# Test a local HTML file
axe index.html --rules wcag2a,wcag2aa
# Test a running server
axe http://localhost:3000/report --rules wcag2a,wcag2aa
Using axe-core programmatically in Node.js with Playwright:
import AxeBuilder from '@axe-core/playwright';
import { chromium } from 'playwright';
async function checkAccessibility(htmlContent) {
const browser = await chromium.launch();
const page = await browser.newPage();
await page.setContent(htmlContent, { waitUntil: 'networkidle' });
const results = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa'])
.analyze();
await browser.close();
if (results.violations.length > 0) {
console.error('Accessibility violations found:');
results.violations.forEach((v) => {
console.error(`[${v.impact}] ${v.id}: ${v.description}`);
v.nodes.forEach((node) => {
console.error(' Element:', node.html);
});
});
return false;
}
console.log('Accessibility check passed.');
return true;
}
7. Common Problems and Solutions
These issues come up frequently when generating accessible PDFs from HTML. For further debugging, see the HTML to PDF troubleshooting guide and PDF API error handling guide.
Problem 1: Screen Reader Cannot Read Text
Cause: Text is embedded as an image, or the font is not embedded in the PDF.
<!-- Bad: text as image, no alt text -->
<img src="heading-text.png" alt="">
<!-- Good: real text styled with CSS -->
<h1 style="font-family: Arial, sans-serif; color: #1a1a2e;">
Report Title
</h1>
For Japanese font embedding issues, see the PDF Japanese font guide.
Problem 2: Reading Order Does Not Match Visual Order
Cause: CSS position: absolute, flexbox flex-direction: row-reverse, or CSS Grid places elements out of DOM order.
<!-- Bad: CSS reverses order, screen reader reads wrong sequence -->
<div style="display: flex; flex-direction: row-reverse;">
<div>Visually first, but DOM second</div>
<div>Visually second, but DOM first</div>
</div>
<!-- Good: DOM order matches visual order -->
<div style="display: flex;">
<div>First</div>
<div>Second</div>
</div>
Problem 3: Table Headers Not Recognized
Cause: Using <td> with <strong> instead of <th>, or missing scope attribute.
<!-- Bad: bold text used as table header -->
<table>
<tr>
<td><strong>Month</strong></td>
<td><strong>Revenue</strong></td>
</tr>
<tr>
<td>October</td>
<td>$98,000</td>
</tr>
</table>
<!-- Good: proper th with scope -->
<table>
<thead>
<tr>
<th scope="col">Month</th>
<th scope="col">Revenue</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">October</th>
<td>$98,000</td>
</tr>
</tbody>
</table>
Problem 4: Insufficient Color Contrast
Cause: Brand colors that look attractive on screen fail WCAG contrast requirements.
/* Failing: light blue on white */
.brand-text {
color: #7cb9e8; /* light blue — contrast ratio 2.0:1 on white */
}
/* Fixed: darker shade of the same color family */
.brand-text {
color: #005a9e; /* dark blue — contrast ratio 7.0:1 on white */
}
Use the WebAIM Contrast Checker to verify ratios before finalizing styles.
Problem 5: Form Labels Disappear
Cause: Using placeholder as the only visible label. Placeholder text disappears when the user starts typing, leaving no label visible.
<!-- Bad: placeholder only -->
<input type="email" placeholder="Enter your email address">
<!-- Good: visible label + helpful placeholder -->
<label for="email">Email Address</label>
<input type="email" id="email" name="email" placeholder="you@company.com">
8. Summary
PDF accessibility is both a legal requirement in many jurisdictions and a design principle: everyone should be able to read the documents you publish.
When generating PDFs from HTML, accessibility starts in the HTML. Use semantic tags, declare language, add alt text, ensure contrast ratios, and structure tables and forms correctly. FUNBREW PDF will then convert that HTML into a tagged PDF that works with screen readers.
Steps to Get Started
- Try an accessible HTML template in the playground
- Run axe-core against your existing HTML templates
- Validate generated PDFs with PAC 2024
- Add the checklist from this article to your development workflow
Related Articles
- HTML to PDF Complete Guide — Full overview of PDF generation
- HTML to PDF CSS Tips — Layout and styling best practices
- PDF Japanese Font Guide — Japanese font configuration
- PDF API Security Guide — Security implementation
- PDF API Production Checklist — Stable production deployment
- HTML to PDF Troubleshooting — Debugging common issues
- PDF API Error Handling Guide — Error handling best practices
- Certificate PDF Automation Guide — Automated certificate generation
- PDF Report Generation Guide — Report design and implementation
- Use Cases — FUNBREW PDF in production
- Pricing — Start with a free trial