Invalid Date

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 lang attribute 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 alt attributes
  • Decorative images have alt="" and role="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" or scope="row"
  • Complex tables use headers attribute 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

  1. Try an accessible HTML template in the playground
  2. Run axe-core against your existing HTML templates
  3. Validate generated PDFs with PAC 2024
  4. Add the checklist from this article to your development workflow

Related Articles

Powered by FUNBREW PDF