A tagged PDF isn't the same as an accessible one. Here's what's inside the tags.

Somewhere in the last approval cycle, somebody told you the PDF has to be "tagged." A procurement checklist, a validator report, an EAA compliance note — the word turns up everywhere in PDF accessibility, and almost nobody explains what a tag actually is or where it lives inside the file. It isn't a label stuck on the outside of the document. It's a second, invisible structure sitting behind the one you see on screen, and the only way to understand what it does is to open one.

8 min read

On this page
  1. What's actually inside
  2. The numbers written on the page
  3. Tags say what something is, not whether it's right
  4. The alternative is worse
  5. What a tag tree can't tell you
  6. Seeing the tree on your own file

Here is a real one: a privacy notice published by a Hungarian village council, five pages, exported from Word 2019 — one of 111 born-digital public-sector PDFs in the sample behind our earlier piece on where Word-exported PDFs actually fail. We picked it because it carries almost everything a tagged PDF can contain in a small space: a title, headings, a table, a list, a figure, a couple of links.

What's actually inside

Every tagged PDF has a structure tree — a hierarchy stored alongside the pages that a screen reader, Acrobat, or a plain PDF library can walk independently of what's drawn on the page. Ours starts like this:

uv run python scripts/dump_struct_tree.py privacy-notice.pdf
Document
  Title MCIDs=[0, 1]
  Figure MCID=30
  Title MCIDs=[2, 3]
  P MCID=4
  P MCID=5
  P MCID=6
  Table
    THead …
    TBody …            (7 rows total — the contact-details table below)
  H3 MCID=24
  P MCID=25
  P MCID=26
  P MCID=27
  P MCID=28
  H3 MCID=29
  P MCID=0
  L [ListNumbering=/Disc]
    LI
      Lbl MCID=1
      LBody MCIDs=[5, 6]
    LI
      Lbl MCID=7
      LBody MCID=10
  H3 MCID=11

Document is the root. The second Title element holds the notice's actual headline, "Weboldal Adatkezelési Tájékoztató" ("Website Privacy Notice") — Word tags a headline this way and maps it to a top-level heading behind the scenes, so a screen reader announces it the way it would announce any other H1. The first Title, MCIDs 0 and 1, turns out to be blank: two marked regions with no characters in them at all, sitting above the badge that comes next. Tagged, styled, empty. Figure marks that badge — a small blue circle with the EU's ring of stars and the word "GDPR" inside it, the kind of clip art every second privacy notice opens with: a picture with a bounding box on the page and, we'll come back to this, nothing else. Table splits into header and body rows, the same shape as an HTML table and for the same reason — it lets a screen reader say "header: postal address" before reading the value next to it, instead of a bare grid. H3 marks the section headings further down the page, the same tags that let a screen reader user jump straight between headings rather than reading everything in between, the way a sighted reader skims a page for the right subheading. L is the list: each LI splits into a Lbl (the bullet itself) and an LBody (what the bullet introduces) — two separate tags for what looks, on the page, like one line of text.

The numbers written on the page

None of that is decoration. Every MCID — Marked Content ID — is a number written directly into the page's own drawing instructions, the same stream of "draw this glyph here" commands that produces the visible page. A tag doesn't contain text; it points at a numbered region of the page and states what that region is. Pull the text bound to a set of MCIDs and you get exactly what a screen reader would read out for that tag, which is what scripts/debug_dump_struct.py does for every table cell:

uv run python scripts/debug_dump_struct.py privacy-notice.pdf
  TABLE #1
    Row 0: n_cells=2
      Cell 0: role=/TH  text=A weboldal üzemeltetője, … (a továbbiakban: Adatkezelő):
      Cell 1: role=/TH  text=[the council's name]
    Row 1: n_cells=1
      Cell 0: role=/TH  text=Elérhetőségeink:
    Row 2: n_cells=2
      Cell 0: role=/TH  text=Postai címünk:
      Cell 1: role=/TD  text=[postal address]

(Row 0 names the data controller — the council that publishes the file. We have replaced the council's name and address here: this article is about the tag tree, not about which village got it wrong, and with 39 clean sites out of 1,215 in our wider crawl this is a systemic pattern rather than one office's failing. Row 1, "Elérhetőségeink" — "our contact details" — is a sub-heading welded into the table as a row with a single cell, where every other row has two.)

That single-cell row is where the tree stops matching the standard, which is the whole point of this article.

Tags say what something is, not whether it's right

A structure tree tells a screen reader what kind of thing it's looking at. It says nothing about whether that classification is complete or correctly formed — and the same file that produced the tree above fails PDF/UA-1 on eight separate rules when checked with veraPDF 1.30.2 (build 2026-06-03):

~/.local/share/verapdf/verapdf -f ua1 --format json privacy-notice.pdf
Clause What it means What veraPDF found here
7.2-43 table rows must share the same column count "Table rows 1 and 2 span different number of columns (2 and 1 respectively)" — the exact row quoted above
7.4.2-1 a heading sequence can't skip a level the document's first heading is tagged H3; H1 and H2 never appear anywhere in the file
7.3-1 figures need alternative text or a replacement "Figure structure element neither has an alternate description nor a replacement text" — the GDPR badge at the top of page one

That gap between "tagged" and "correct" is exactly what WCAG's own technique for adding alt text inside a tagged PDF is written against: when an image carries a word that matters — "GDPR," in this case, sitting right there in the badge's own pixels — that word belongs in the /Alt entry, not just on the page. Without it, a screen reader reaching this Figure has no filename to fall back on and no caption to read; just a picture tag with nothing behind it.

Eight rules, 36 individual failed checks in total, against 17,049 checks the same file does pass. It has a title, a marked-content flag, a declared language, a table, a list, a heading structure — genuinely tagged, by any definition a checklist would accept — and it still isn't a conforming file. "Has tags" and "conforms to PDF/UA-1" are two different claims, and only one of them is what the standard actually requires. What each clause means in general, and which ones repair automatically, is in our PDF/UA error reference; what the standard requires end to end, and how it relates to WCAG, is in the PDF/UA guide.

The alternative is worse

Set that file next to one with no tree at all. A benefit-application form from the same sample — "KÉRELEM az ápolási díj megállapítására," a request for a nursing-care allowance — was produced through Windows' own Print to PDF driver rather than Word's PDF export. Run the same command against it:

uv run python scripts/dump_struct_tree.py apolasi-dij-kerelem.pdf
### apolasi-dij-kerelem.pdf: no StructTreeRoot ###

There is no tree to walk. Every character on the page carries mcid: None — pdfplumber can still pull the text out, because the glyphs are real text and not a scanned image, but nothing marks which characters form the numbered heading "1.1.1. Neve:" ("1.1.1. Name:") versus the blank line meant for an answer. Visually, the numbering makes the structure obvious. To a screen reader, every one of those lines is the same kind of nothing.

What a tag tree can't tell you

Look again at row 0 of that contact table. Both cells are tagged /TH — a header cell — even though the second one holds the council's actual name, not a label. Nothing in the standard requires a /TH to genuinely introduce something; the row/column check that flagged row 1 fires on a mismatched count, not on whether a header cell is really a header. Whether it is stays a judgement call no validator makes.

That's the trap in trusting a tree view on its own. A tags panel — Acrobat's or PAC's — renders exactly the tree we've been reading here: correctly nested, properly labelled, easy to mistake for "done." It doesn't run the row-count check, the heading-sequence check, or the alt-text check against what it's showing you; those are a separate validation pass, over the same file, against the same tree. A tree that looks complete and a file that conforms are two different claims, and a tree view only ever answers the first one.

Seeing the tree on your own file

How to open a tag tree without a terminal

Adobe Acrobat Pro (Windows, macOS)

View → Show/Hide → Navigation Panes → Tags opens the same kind of tree shown above. Click any tag and Acrobat highlights the matching region on the page — the fastest way to see whether a heading, table or figure actually points at what you think it does.

PAC (PDF Accessibility Checker, free, Windows)

PAC's "Logical Structure" view shows the tree alongside every PDF/UA-1 failure it finds, which is closer to what this article did than Acrobat's tree view on its own.

No installation

Our own PDF accessibility checker reports the same clause-level failures veraPDF does, from a browser, and the check is free.

If a document has come back from a validator with a list of clause numbers, that's the second half of this article, not the first — the error reference linked above explains what each one means. If a document has never been checked at all, that's worth doing before assuming the tags, if there are any, are enough.

Zoltán Csordás

Founder, a11yfy

Zoltán Csordás is an accessibility engineer and the founder of a11yfy. He builds the PDF/UA remediation pipeline behind a11yfy.com and spends most of his week inside tag trees, veraPDF reports and screen-reader output. In 2026 he measured 8,469 PDFs across 1,682 Hungarian municipal websites to find out how bad the problem really is. Based in Budapest.